Compare commits
81 Commits
feature/OR
...
feature/OR
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72d662ae88 | ||
|
|
348cf8c164 | ||
| bc2347abd3 | |||
| 62c1fe3461 | |||
| 0dfddf93f0 | |||
| 22d3b77426 | |||
| 4a06537afd | |||
| b6c0e11e4d | |||
| 3fb3d15cb4 | |||
| 4815e378d9 | |||
| 67e98b8296 | |||
|
|
cad5e98892 | ||
| bb03350ec9 | |||
| 930e65298c | |||
| cba67a4270 | |||
| 720c31393a | |||
| 9b7c855df3 | |||
| a6b444c356 | |||
| dbf14e3d5a | |||
| 4bebb921ff | |||
| 9f846b5a50 | |||
| b760b24a48 | |||
| f0ac9d5562 | |||
| 987ea810bf | |||
| f85e449d80 | |||
| 1c89ac9df9 | |||
| 03d899812c | |||
| b9bcdc1545 | |||
| b04fae748e | |||
| fbfcd84b16 | |||
| 2f4c553fd8 | |||
| 2bdba532d5 | |||
| db83b89467 | |||
| 961c5e9eee | |||
| 84a6f61ba8 | |||
| 1af356a343 | |||
| e18947d2d9 | |||
| 0ec34d10fc | |||
| bf6a0c095a | |||
| 39769bdf23 | |||
| de47737f4f | |||
|
|
e3f7c1c272 | ||
|
|
32a7aa8c6b | ||
|
|
fe8586ed78 | ||
| 9070489968 | |||
| 1d1208c136 | |||
| 3ab2690a68 | |||
| 3806522041 | |||
| d4c6cc0f61 | |||
| 210aef6954 | |||
| 1820b0244e | |||
| 2f898ede7b | |||
| 829b914ff7 | |||
| 55e5e968ae | |||
| 4db8276f98 | |||
| efe437a4aa | |||
| 365c67f45d | |||
| d6e0df3550 | |||
| 4d4f542b71 | |||
| 9e810c89f0 | |||
| 60e5596e94 | |||
| bf60f7a48a | |||
| 637c4e9e2e | |||
| 094b5e2f96 | |||
| 90b6c8d5a8 | |||
| 2221d402b1 | |||
| 6ddff5583d | |||
|
|
c53d625744 | ||
| 2ee06ae676 | |||
| 3b3d587300 | |||
|
|
f0c2986477 | ||
| 83397570fe | |||
| dbc32fc106 | |||
| 282636fedb | |||
| e5f9c38e65 | |||
|
|
e4c6401633 | ||
|
|
115519ebb4 | ||
|
|
64e031a37f | ||
| 01ff71978f | |||
|
|
d5915a89b9 | ||
| 1ff8d85bb9 |
81
.env.example
81
.env.example
@@ -72,6 +72,29 @@ ORCH_DEPLOY_PROD_TARGET_IMAGE=orchestrator-orchestrator
|
||||
ORCH_DEPLOY_PROD_COMPOSE_PROFILE=
|
||||
ORCH_DEPLOY_PROD_PREV_IMAGE_FILE=.deploy-prev-image-prod
|
||||
|
||||
# ORCH-058: staging-image provenance before the BUILD-ONCE prod retag (INV-FRESH).
|
||||
# Guarantees the staging image promoted to prod is the EXACT artefact rebuilt from the
|
||||
# validated commit — two layers, self-hosting only:
|
||||
# A (liveness): QG sub-check `check_staging_image_fresh` on the deploy-staging->deploy
|
||||
# edge rebuilds orchestrator-orchestrator-staging from the validated commit + recreates
|
||||
# 8501; FAIL -> rollback to development. (builds/recreate STAGING only, never prod.)
|
||||
# B (safety): the Dockerfile stamps `org.opencontainers.image.revision`; the prod hook
|
||||
# fail-closes (exit 1) before `docker tag` if SOURCE_IMAGE's label != EXPECTED_REVISION.
|
||||
# ENABLED -> single kill-switch for A+B as a WHOLE (never "B without A"); false -> legacy.
|
||||
# REPOS -> CSV of repos where the gate is REAL; empty -> only self-hosting (orchestrator).
|
||||
ORCH_IMAGE_FRESHNESS_ENABLED=true
|
||||
ORCH_IMAGE_FRESHNESS_REPOS=
|
||||
|
||||
# ORCH-061: staging-verdict tolerance to sandbox-infra-only FAILs. The self-hosting
|
||||
# orchestrator looped on deploy-staging because staging_check.py exited 1 on ANY FAIL,
|
||||
# so two infra-only checks (C9a sandbox branch / C9b analyst-job — caused by SANDBOX
|
||||
# bot accounts not being members of the sandbox Plane project, NOT a pipeline regress)
|
||||
# forced staging_status: FAILED -> rollback -> loop. With this ON, C9a/C9b are WAIVED
|
||||
# to SUCCESS when every REAL check is green; any REAL failure still fails closed.
|
||||
# true (default) -> tolerant; false -> legacy strict (1:1 pre-ORCH-061, any FAIL rolls back).
|
||||
# Lives in .env.staging (the staging instance). CLI --strict overrides this per-run.
|
||||
ORCH_STAGING_INFRA_TOLERANCE_ENABLED=true
|
||||
|
||||
# ORCH-053: stuck-task reconciler (sweeper for lost webhooks). A background daemon
|
||||
# replays a missed stage transition through the SAME gates/handlers a webhook would,
|
||||
# fixing tasks that got stuck on a dropped event (502 on rebuild, no Plane/Gitea
|
||||
@@ -82,9 +105,67 @@ ORCH_DEPLOY_PROD_PREV_IMAGE_FILE=.deploy-prev-image-prod
|
||||
# GRACE_DEFAULT_S -> default "stuck" threshold on tasks.updated_at (seconds).
|
||||
# GRACE_OVERRIDES_JSON -> per-stage thresholds, e.g. {"development":300}; bad JSON -> default.
|
||||
# NOTIFY_UNBLOCK -> send a Telegram message when a stuck task is unblocked.
|
||||
# SKIP_BLOCKED_ENABLED -> ORCH-060 F-1 Guard 2: skip reconciling issues a human moved
|
||||
# to Blocked / Needs Input (per-candidate Plane state lookup).
|
||||
# false mutes ONLY the networked Guard 2; Guard 1 (escalated by
|
||||
# developer retries, local+deterministic) is always active.
|
||||
ORCH_RECONCILE_ENABLED=true
|
||||
ORCH_RECONCILE_PLANE_ENABLED=true
|
||||
ORCH_RECONCILE_INTERVAL_S=120
|
||||
ORCH_RECONCILE_GRACE_DEFAULT_S=600
|
||||
ORCH_RECONCILE_GRACE_OVERRIDES_JSON=
|
||||
ORCH_RECONCILE_NOTIFY_UNBLOCK=true
|
||||
ORCH_RECONCILE_SKIP_BLOCKED_ENABLED=true
|
||||
|
||||
# ORCH-065: job-reaper + proactive merge-lease reclaim. A background daemon thread
|
||||
# (src/job_reaper.py, started LAST in main.lifespan after requeue_running_jobs) reaps
|
||||
# zombie 'running' jobs whose monitor/process died before writing the terminal status
|
||||
# (one zombie at max_concurrency=1 blocks the whole shared queue) and periodically
|
||||
# reclaims dead/stale merge-leases. Liveness is three-tier: Tier-1 dead jobs.pid
|
||||
# (os.kill(pid,0)) after REAPER_DEAD_TICKS consecutive dead ticks (anti-false-positive
|
||||
# for a live agent); Tier-2 agent_runs.exit_code recorded but job still 'running'
|
||||
# (only after a REAPER_FINALIZE_GRACE_S finalization grace, so a live monitor still
|
||||
# doing git push / PR / Plane comments is never reaped); Tier-3 backstop after
|
||||
# REAPER_MAX_RUNNING_S. The terminal flip carries an atomic status='running' guard and
|
||||
# precedes any advance/enqueue (claim-before-act) so it never double-processes/-advances
|
||||
# a row racing a late monitor or requeue_running_jobs.
|
||||
# REAPER_ENABLED -> global kill-switch (false -> strictly prior behaviour).
|
||||
# REAPER_INTERVAL_S -> background scan period (seconds).
|
||||
# REAPER_DEAD_TICKS -> consecutive dead-pid ticks before reaping (Tier-1, >=2).
|
||||
# REAPER_MAX_RUNNING_S -> Tier-3 backstop ceiling; must exceed max agent_timeout+grace.
|
||||
# REAPER_FINALIZE_GRACE_S -> Tier-2 grace: how long agent_runs.exit_code must have been
|
||||
# recorded before a still-'running' job is reaped; MUST exceed
|
||||
# the max finalization window (git push + PR + Plane comments).
|
||||
# LEASE_RECLAIM_ENABLED -> kill-switch for the proactive stale/dead lease reclaim
|
||||
# (false -> only the legacy lazy TTL reclaim in acquire_merge_lease).
|
||||
# (reuse) ORCH_MERGE_LOCK_TIMEOUT_S -> lease TTL; ORCH_MERGE_GATE_REPOS -> reclaim scope.
|
||||
ORCH_REAPER_ENABLED=true
|
||||
ORCH_REAPER_INTERVAL_S=60
|
||||
ORCH_REAPER_DEAD_TICKS=2
|
||||
ORCH_REAPER_MAX_RUNNING_S=3600
|
||||
ORCH_REAPER_FINALIZE_GRACE_S=300
|
||||
ORCH_LEASE_RECLAIM_ENABLED=true
|
||||
|
||||
# ORCH-021: post-deploy production monitoring + degradation reaction. After the
|
||||
# terminal deploy->done transition for an applicable repo, a reserved-agent job
|
||||
# `post-deploy-monitor` (no LLM, modelled on deploy-finalizer) probes prod over a
|
||||
# window and reacts to a degradation the restart-time health-check missed (class
|
||||
# "green deploy, red prod", precedent ET-8). State is in sentinel files
|
||||
# (.post-deploy-state-<repo>/<wi>/), no DB migration.
|
||||
# MONITOR_ENABLED -> global kill-switch; false -> pipeline is 1:1 as before ORCH-021.
|
||||
# REPOS -> CSV of repos where monitoring is REAL; empty -> only self-hosting.
|
||||
# WINDOW_S -> observation window length (~15 min).
|
||||
# INTERVAL_S -> seconds between probe ticks.
|
||||
# FAIL_THRESHOLD -> N CONSECUTIVE health failures -> DEGRADED.
|
||||
# 5XX_THRESHOLD -> window 5xx ratio above this -> DEGRADED.
|
||||
# AUTO_ROLLBACK -> allow auto-rollback; acts ONLY for non-self repos. Self-hosting
|
||||
# is ALWAYS ALERT_ONLY (a tick NEVER restarts the prod container).
|
||||
# BASE_URL -> base URL of the observed prod instance.
|
||||
ORCH_POST_DEPLOY_MONITOR_ENABLED=true
|
||||
ORCH_POST_DEPLOY_REPOS=
|
||||
ORCH_POST_DEPLOY_WINDOW_S=900
|
||||
ORCH_POST_DEPLOY_INTERVAL_S=30
|
||||
ORCH_POST_DEPLOY_FAIL_THRESHOLD=3
|
||||
ORCH_POST_DEPLOY_5XX_THRESHOLD=0.5
|
||||
ORCH_POST_DEPLOY_AUTO_ROLLBACK=false
|
||||
ORCH_POST_DEPLOY_BASE_URL=http://localhost:8500
|
||||
|
||||
@@ -37,8 +37,19 @@ On stage `deploy-staging` your job is to run the staging test suite and write a
|
||||
not exist. Details: `docs/operations/STAGING_CHECK.md`.
|
||||
|
||||
2. Check the exit code:
|
||||
- Exit code **0** = all tests PASS → `staging_status: SUCCESS`
|
||||
- Exit code **non-zero** = tests FAILED → `staging_status: FAILED`
|
||||
- Exit code **0** = advance → `staging_status: SUCCESS`
|
||||
- Exit code **non-zero** = rollback → `staging_status: FAILED`
|
||||
|
||||
> **ORCH-061**: exit 0 may now include *waived* sandbox-infra failures. The two
|
||||
> infra-only checks **C9a/C9b** (sandbox branch / analyst-job, which depend on
|
||||
> SANDBOX bot accounts being project members — not on the pipeline) are tolerated
|
||||
> when every REAL check is green; the script prints an `INFRA-WAIVED:` line and a
|
||||
> `VERDICT:` line, and still exits 0. Any REAL check failing still yields exit 1
|
||||
> (fail-closed). If you see `INFRA-WAIVED:` in the output, copy that line into the
|
||||
> `15-staging-log.md` body for observability. The exit-code → `staging_status`
|
||||
> mapping above is unchanged: trust the exit code, do NOT re-judge waived checks.
|
||||
> Kill-switch: `ORCH_STAGING_INFRA_TOLERANCE_ENABLED=false` (or `--strict`) restores
|
||||
> legacy strictness. Details: `docs/operations/STAGING_CHECK.md`.
|
||||
|
||||
3. Write the verdict to `docs/work-items/<work_item_id>/15-staging-log.md` with YAML frontmatter:
|
||||
```markdown
|
||||
@@ -80,6 +91,30 @@ The verdict contract is unchanged: `docs/work-items/<work_item_id>/14-deploy-log
|
||||
frontmatter field `deploy_status: SUCCESS|FAILED` (the gate `check_deploy_status` parses ONLY this).
|
||||
**What changed (ORCH-36): WHO and WHEN writes that verdict, for the self-hosting repo.**
|
||||
|
||||
### ⚠️ Idempotent merge guard — consult `pr_already_merged` BEFORE merging (ORCH-065)
|
||||
|
||||
The `deploy` stage can be **re-driven**: if a process/monitor thread died after the PR
|
||||
merged but before the job finalised, the job-reaper requeues it and this stage runs **again**
|
||||
(ADR-001 ORCH-065, Р-3). A blind second merge of an already-merged PR makes Gitea return a
|
||||
merge error → a false БАГ-8 rollback. To stay idempotent, **before you merge the feature
|
||||
branch PR into `main`, consult the deterministic guard** `merge_gate.pr_already_merged(repo, branch)`:
|
||||
|
||||
```bash
|
||||
# Already merged? exit 0 = yes (skip the merge), exit 1 = no (merge normally).
|
||||
python3 -c "import sys; from src.merge_gate import pr_already_merged; \
|
||||
sys.exit(0 if pr_already_merged('<repo>', '<branch>') else 1)" && MERGED=1 || MERGED=0
|
||||
```
|
||||
|
||||
- `MERGED=1` (PR already merged) → **do NOT merge again** (no second merge, no error).
|
||||
Treat the merge as already done and continue to write the deploy verdict
|
||||
(`deploy_status: SUCCESS` once the deploy itself is health-ok). This is the AC-11 no-op.
|
||||
- `MERGED=0` (not merged) → merge the PR normally, then proceed.
|
||||
|
||||
The guard is **never-raise** (any Gitea/parse error → `False` → "not known-merged", so a real
|
||||
merge is never silently skipped). This is the single consultation point ADR-001 Р-3 /
|
||||
README / CHANGELOG refer to: the **merge path (deployer/merge) consults the guard before a
|
||||
(repeat) merge**.
|
||||
|
||||
### Self-hosting repo (`orchestrator`) — you do NOT deploy yourself
|
||||
|
||||
For `orchestrator` the `deploy` stage is orchestrated by **deterministic code** in
|
||||
@@ -113,4 +148,7 @@ deploys go through `scripts/orchestrator-deploy-hook.sh` (parametrised; defaults
|
||||
|
||||
- Always write machine-readable YAML frontmatter — the quality gates parse ONLY the frontmatter fields, never the body prose.
|
||||
- Never push directly to `main`. Always use a PR or the artifact merge pattern.
|
||||
- **Idempotent merge (ORCH-065):** before any (re-)merge of a feature PR into `main`, consult
|
||||
`merge_gate.pr_already_merged(repo, branch)` (see the `deploy` stage section). Already merged
|
||||
→ no second merge, no error — the stage is a no-op on the merge and proceeds to its verdict.
|
||||
- Never modify `.env`, `.env.staging`, `docker-compose.yml`, or production infrastructure.
|
||||
|
||||
4
.task-arch.md
Normal file
4
.task-arch.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Work item: ORCH-061
|
||||
Repo: orchestrator
|
||||
Branch: feature/ORCH-061-bug-deploy-staging-development
|
||||
Stage: architecture
|
||||
4
.task-dev.md
Normal file
4
.task-dev.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Work item: ORCH-061
|
||||
Repo: orchestrator
|
||||
Branch: feature/ORCH-061-bug-deploy-staging-development
|
||||
Stage: development
|
||||
8
.task.md
Normal file
8
.task.md
Normal file
@@ -0,0 +1,8 @@
|
||||
Work item: ORCH-061
|
||||
Repo: orchestrator
|
||||
Branch: feature/ORCH-061-bug-deploy-staging-development
|
||||
Stage: analysis
|
||||
Title: BUG: deploy-staging петля — откат на development (self-deploy)
|
||||
|
||||
Description:
|
||||
Симптом: на стадии deploy-staging для self-hosting orchestrator задача откатывается deploy-staging -> development и крутится по кругу.ДВЕ подтверждённые причины (ORCH-58 + ORCH-60):1. check_staging_status FAILED (ложный). deployer гоняет staging_check.py, тот падает на C9a/C9b (sandbox e2e: branch not found + analyst job in queue) с пометкой «Plane comment check skipped: bot-tokens not added to SANDBOX project». 8/10 PASS, 2 ложных FAIL из-за ненастроенных bot-токенов SANDBOX-проекта. QG check_staging_status -> FAILED -> rollback deploy-staging->development. Это НЕ регресс кода, а отсутствие sandbox-настроек.2. no changes to commit. для action-стадий (деплой = рестарт/retag, не правка кода) deployer exit0 + «no changes» тоже трактуется stage_engine как недовыполнение -> откат.Последствие: прод-деплой self-hosting репо НЕВОЗМОЖЕН автономно — ORCH-58 и ORCH-60 доводились ВРУЧНУЮ (merge PR + build-once retag + --deploy). Прямой блокер автономного внедрения (эпик ORCH-54).Fix-направления (одно или оба):(а) Настроить sandbox bot-токены в SANDBOX Plane-проект, чтобы staging_check C9a/C9b проходили честно (10/10). Тогда check_staging_status не будет ложно падать.(б) Отвязать advance deploy-стадии от git-changes для self-deploy репо: успех = exit0 + health PASS (+ опц. staging_check), а не наличие коммита.Acceptance: ORCH-задача для self-hosting orchestrator проходит deploy-staging -> deploy -> Done БЕЗ ручного вмешательства и без петли. Priority P0.
|
||||
File diff suppressed because one or more lines are too long
@@ -38,6 +38,9 @@ created → analysis → architecture → development → review → testing →
|
||||
└──── REQUEST_CHANGES ──────┘ (откат на development, max 3)
|
||||
```
|
||||
|
||||
## Статусная модель Plane (ORCH-066) — индикация ≠ управление
|
||||
Статусы Plane — это **слой B (индикация)**, отдельный от **слоя A (машина стадий)** `src/stages.py::STAGE_TRANSITIONS`. Plane показывает наблюдателю осмысленную картину (`Backlog → Todo → Analysis → Architecture → Development → Code-Review → Testing → Awaiting Deploy → Deploying → Monitoring after Deploy → Done` + человеческие гейты `In Review/Approved`, `Confirm Deploy`), но НИКОГДА не управляет конвейером. Маппинг и сеттеры — `src/plane_sync.py` (6 новых ключей: `to_analyse/analysis/code_review/awaiting_deploy/deploying/monitoring`), с project-relative alias-fallback: на частично сконфигурированном проекте новый ключ деградирует на базовый UUID ТОГО ЖЕ проекта (нулевая регрессия для enduro-trails). Детали — `docs/architecture/README.md`.
|
||||
|
||||
## Конвенции
|
||||
- Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`)
|
||||
- Ветки: `feature/ORCH-NNN-slug`, `fix/ORCH-NNN-slug`
|
||||
@@ -47,7 +50,7 @@ created → analysis → architecture → development → review → testing →
|
||||
- Машинные вердикты Quality Gate — строго YAML-frontmatter (`verdict:`, `deploy_status:`, `staging_status:`), никогда проза
|
||||
|
||||
## Артефакты задачи (`docs/work-items/<plane-id>/`)
|
||||
`00-business-request.md`, `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml`, `06-adr/ADR-NNN-slug.md`, `07-infra-requirements.md`, `08-data-requirements.md`, `10-tech-risks.md`, `12-review.md`, `13-test-report.md`, `14-deploy-log.md`, `15-staging-log.md`.
|
||||
`00-business-request.md`, `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml`, `06-adr/ADR-NNN-slug.md`, `07-infra-requirements.md`, `08-data-requirements.md`, `10-tech-risks.md`, `12-review.md`, `13-test-report.md`, `14-deploy-log.md`, `15-staging-log.md`, `16-post-deploy-log.md` (post-deploy наблюдение, ORCH-021).
|
||||
|
||||
## Правила для агентов
|
||||
1. Перед любым действием прочесть этот файл и `docs/architecture/README.md`.
|
||||
|
||||
23
Dockerfile
23
Dockerfile
@@ -1,11 +1,32 @@
|
||||
FROM python:3.12-slim
|
||||
# ORCH-058 (Strategy B): stamp the image with the git commit it was built from so
|
||||
# the deploy hook can fail-close if a stale staging image would be promoted to prod
|
||||
# (INV-FRESH). Passed at build time via `--build-arg GIT_SHA=<sha>` (the staging
|
||||
# rebuild in check_staging_image_fresh / the --build-staging hook mode supplies it).
|
||||
# Without the build-arg the label is empty -> the hook treats it as a mismatch
|
||||
# (fail-closed). The OCI-standard key is read by `docker image inspect`.
|
||||
ARG GIT_SHA=""
|
||||
LABEL org.opencontainers.image.revision=$GIT_SHA
|
||||
WORKDIR /app
|
||||
RUN apt-get update -qq && apt-get install -y -qq openssh-client git && rm -rf /var/lib/apt/lists/*
|
||||
# git operations run as root over bind-mounted /repos (may be owned by host uid) -> trust it.
|
||||
RUN git config --system --add safe.directory '*'
|
||||
# ORCH-58: compose runs the container as uid:gid 1000:1000 (ORCH-40), but the base
|
||||
# image has no passwd entry for uid 1000 -> ssh/whoami fail with
|
||||
# "No user exists for uid 1000" (rc=255), breaking the detached self-deploy ssh
|
||||
# launch (ORCH-36 Phase B). Create a real user 1000 with a home dir so getpwuid()
|
||||
# resolves and ssh can start.
|
||||
RUN groupadd -g 1000 app && useradd -u 1000 -g 1000 -m -d /home/slin -s /bin/bash slin
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY src/ ./src/
|
||||
COPY data/ ./data/
|
||||
# ORCH-021: do NOT `COPY data/ ./data/`. `data/` is gitignored (SQLite DB dir) and
|
||||
# is provided at runtime as a bind-mount volume (`./data:/app/data`, see
|
||||
# docker-compose.yml) which shadows anything baked into the image — so the COPY was
|
||||
# dead weight. Worse, the ORCH-058 staging rebuild (`check_staging_image_fresh`)
|
||||
# builds with the task *worktree* as the docker build context; a fresh worktree never
|
||||
# contains the untracked `data/`, so `COPY data/` failed `docker build` with exit 1
|
||||
# and bounced the task off `deploy-staging`. We just ensure the mountpoint exists.
|
||||
RUN mkdir -p /app/data
|
||||
ENV PYTHONPATH=/app
|
||||
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8500"]
|
||||
|
||||
@@ -135,6 +135,7 @@ uvicorn src.main:app --reload --port 8500
|
||||
| `ORCH_RECONCILE_GRACE_DEFAULT_S` | Порог «застряла» по `tasks.updated_at`, сек | `600` |
|
||||
| `ORCH_RECONCILE_GRACE_OVERRIDES_JSON` | Per-stage пороги, напр. `{"development":300}` | `""` |
|
||||
| `ORCH_RECONCILE_NOTIFY_UNBLOCK` | Telegram при разблокировке застрявшей задачи | `true` |
|
||||
| `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED` | F-1 Guard 2 (ORCH-060): пропуск задач в Plane-статусе Blocked / Needs Input; `false` глушит только сетевой Guard 2 (Guard 1 escalated всегда активен) | `true` |
|
||||
|
||||
## Очередь задач (ORCH-1 / F-2b)
|
||||
|
||||
|
||||
@@ -26,9 +26,15 @@ services:
|
||||
environment:
|
||||
- ORCH_REPOS_DIR=/repos
|
||||
- ORCH_HOST_REPOS_DIR=/home/slin/repos
|
||||
# legacy enduro deployer (read via os.environ, keep as-is):
|
||||
- DEPLOY_SSH_USER=slin
|
||||
- DEPLOY_SSH_HOST=127.0.0.1
|
||||
- DEPLOY_HOOK_SCRIPT=/home/slin/bin/enduro-deploy-hook.sh
|
||||
# ORCH-036 self-deploy (read via pydantic ORCH_ prefix; host-network -> 127.0.0.1, ssh key mounted):
|
||||
- ORCH_DEPLOY_SSH_USER=slin
|
||||
- ORCH_DEPLOY_SSH_HOST=127.0.0.1
|
||||
- ORCH_DEPLOY_HOOK_SCRIPT=scripts/orchestrator-deploy-hook.sh
|
||||
- ORCH_DEPLOY_HOST_REPO_PATH=/home/slin/repos/orchestrator
|
||||
group_add:
|
||||
- "999"
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
- **Quality Gates** (`src/qg/checks.py`) — проверки выхода со стадии, реестр `QG_CHECKS`.
|
||||
- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance.
|
||||
- **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe.
|
||||
- **Reconciler** (`src/reconciler.py`, ORCH-053 — реализовано, [adr-0007](adr/adr-0007-reconciler.md)) — фоновый daemon-поток (паттерн `queue_worker`), стартует/останавливается в `main.lifespan` (после `worker.start()` / перед `worker.stop()`). Реконсилирует рассинхрон «источник истины ≠ стадия задачи» при потерянном webhook. F-1 gate-side (продвигает застрявшую стадию по локальной БД через штатный `advance_stage(..., finished_agent=None)`), F-2 plane-side (опрос Plane API → `handle_*` из `plane.py`), F-3 (БД-fallback `sha→branch` в `handle_ci_status`). Источник истины — гейт/Plane, не событие; идемпотентность (active-job guard + atomic-claim + grace); kill-switch `ORCH_RECONCILE_ENABLED`. `analysis` F-1 не трогает (человеческий гейт). Наблюдаемость — блок `reconcile` в `GET /queue`.
|
||||
- **Job-reaper** (`src/job_reaper.py`, ORCH-065 — [adr-0011](adr/adr-0011-job-reaper-lease-reclaim.md)) — фоновый daemon-поток (каркас `reconciler`), стартует/останавливается в `main.lifespan` (после `reconciler.start()` / перед `worker.stop()`). Детектирует «мёртвый» `running`-job **без рестарта** процесса (Tier-1 мёртвый `jobs.pid` после `reaper_dead_ticks` тиков; Tier-2 `agent_runs.exit_code` записан, а job ещё `running`; Tier-3 backstop `reaper_max_running_s`) и приводит строку к корректному статусу через те же контракты (`_try_advance_stage`/`_finalize_job`, gate-driven; exit≠0/неизвестно → `attempts<max`→`queued`, иначе `failed`+Telegram). Атомарный reap-claim (guard `status='running'`) совместим со стартовым `requeue_running_jobs`. Тот же поток периодически делает проактивный реклейм stale/dead merge-lease (см. ниже). never-raise; kill-switch `ORCH_REAPER_ENABLED`; снимок в `GET /queue` (блок `reaper`).
|
||||
- **Reconciler** (`src/reconciler.py`, ORCH-053 — реализовано, [adr-0007](adr/adr-0007-reconciler.md)) — фоновый daemon-поток (паттерн `queue_worker`), стартует/останавливается в `main.lifespan` (после `worker.start()` / перед `worker.stop()`). Реконсилирует рассинхрон «источник истины ≠ стадия задачи» при потерянном webhook. F-1 gate-side (продвигает застрявшую стадию по локальной БД через штатный `advance_stage(..., finished_agent=None)`), F-2 plane-side (опрос Plane API → `handle_*` из `plane.py`), F-3 (БД-fallback `sha→branch` в `handle_ci_status`). Источник истины — гейт/Plane, не событие; идемпотентность (active-job guard + atomic-claim + grace); kill-switch `ORCH_RECONCILE_ENABLED`. `analysis` F-1 не трогает (человеческий гейт). F-1 также пропускает escalated (retry≥лимита) и Blocked/Needs-Input задачи (ORCH-060). Наблюдаемость — блок `reconcile` в `GET /queue`.
|
||||
- **Project Registry** (`src/projects.py`, ORCH-6) — Plane project id → repo + prefix; фильтрация вебхуков по проекту.
|
||||
- **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane.
|
||||
|
||||
@@ -35,13 +36,23 @@ created → analysis → architecture → development → review → testing →
|
||||
| deploy | — | `check_deploy_status` | 14-deploy-log.md (`deploy_status:`) |
|
||||
| done | — | — | — |
|
||||
|
||||
**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043).
|
||||
**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043), check_staging_image_fresh (ORCH-058).
|
||||
|
||||
**Канон гейтов:** машинные вердикты читаются ТОЛЬКО из YAML-frontmatter, никогда из прозы. Лог-файлы мержатся в `origin/main` отдельным PR; гейт читает из `origin/main`.
|
||||
|
||||
### Условный staging-гейт (ORCH-35)
|
||||
`check_staging_status` реален только для self-hosting (`is_self_hosting_repo(repo)` → `orchestrator`); для остальных проектов → no-op `(True, "Staging gate N/A")`. Для orchestrator парсит `staging_status:` из `15-staging-log.md`; FAILED → откат на `development`. Подробнее: [ADR-0003](adr/adr-0003-staging-gate.md).
|
||||
|
||||
### Толерантность staging-вердикта к инфра-FAIL (ORCH-061 — design)
|
||||
Self-hosting зацикливался на `deploy-staging`: `scripts/staging_check.py` давал ложный FAILED на C9a/C9b (ветка в sandbox / analyst-job в очереди), вызванный **отсутствием sandbox-настроек** (bot-аккаунты не члены SANDBOX-проекта), а не регрессом кода → откат `deploy-staging → development` → петля. ORCH-061 классифицирует проверки suite на **REAL** (pipeline) и **SANDBOX_INFRA** (узкий allowlist `{C9a, C9b}`) и делает вердикт толерантным к инфра-FAIL, сохраняя fail-closed для реальных проверок:
|
||||
- Чистая логика — leaf-модуль `src/staging_verdict.py` (`classify_check`, `compute_staging_verdict`, never-raise). Упала хоть одна REAL → FAILED/exit1; упали ТОЛЬКО SANDBOX_INFRA и толерантность вкл → SUCCESS/exit0 (waived); waiver применяется только когда все REAL (вкл. C7/C8) зелёные.
|
||||
- `scripts/staging_check.py` помечает проверки категориями, считает вердикт через `staging_verdict`, печатает `INFRA-WAIVED` (наблюдаемость).
|
||||
- Kill-switch `staging_infra_tolerance_enabled` (env `ORCH_STAGING_INFRA_TOLERANCE_ENABLED`, дефолт `true`, в `.env.staging`); `false` → 1:1 прежнее строгое поведение.
|
||||
- `check_staging_status` / `_parse_staging_status` / `STAGE_TRANSITIONS` / реестр `QG_CHECKS` — **без изменений** (новый QG-чек не вводится); условность ORCH-35 и схема БД сохранены.
|
||||
- Инвариант: «no changes to commit» на action-стадиях (`deploy-staging`/`deploy`) не есть недовыполнение — продвижение определяется exit0 + гейт-вердиктом (launcher не откатывает; добавлена observability-строка).
|
||||
|
||||
Подробнее: [adr-0009](adr/adr-0009-staging-infra-tolerance.md), детально — `docs/work-items/ORCH-061/06-adr/ADR-001-staging-infra-tolerance.md`.
|
||||
|
||||
### Merge-gate: догон `main` + re-test + сериализация слияний (ORCH-043)
|
||||
Детерминированный под-гейт (`check_branch_mergeable`, без LLM) на ребре **`deploy-staging → deploy`**: исполняется ПОСЛЕ `check_staging_status` и ДО запуска deployer'а, который вливает PR в `main` (deployer мержит в начале стадии `deploy`). Стадии (`STAGE_TRANSITIONS`) НЕ меняются — это «под-гейт» ребра, а не отдельная стадия (триггер — то же событие «staging-deployer завершился»).
|
||||
|
||||
@@ -80,6 +91,70 @@ terminal-sync, merge-gate, exit-code-контракт хука. Restart-safe с
|
||||
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`.
|
||||
|
||||
### Post-deploy наблюдение прода + реакция на деградацию (ORCH-021 — реализовано)
|
||||
Конвейер заканчивался на `deploy → done` и **забывал про прод**: «успех» = health-check
|
||||
в момент рестарта (~60с). Класс «зелёный деплой, красный прод» (прецедент ET-8 —
|
||||
деградация через минуты под трафиком, health `200 ok`, фича сломана). ORCH-021 продлевает
|
||||
ответственность **ЗА** `done`: для применимого репо после терминального перехода армится
|
||||
наблюдение окна `post_deploy_window_s` (~15 мин) с интервалом `post_deploy_interval_s`;
|
||||
деградация фиксируется по детерминированным порогам, при подтверждении — реакция.
|
||||
|
||||
Механизм — **reserved-agent job `post-deploy-monitor`** (калька `deploy-finalizer`, НЕ
|
||||
стадия и НЕ daemon): арм в `advance_stage` в блоке `next_stage == "done"`
|
||||
(`post_deploy.arm_monitor`, sentinel `armed` = идемпотентность); тик перехватывается в
|
||||
`launcher.launch_job` ДО `_spawn` → `stage_engine.run_post_deploy_monitor` (один опрос →
|
||||
append в `series` → классификация → перепостановка с задержкой ИЛИ реакция+артефакт+`done`).
|
||||
Чистая логика — новый leaf-модуль `src/post_deploy.py` (never-raise): `post_deploy_applies`,
|
||||
`probe_signals` (`/health` 200+`{"status":"ok"}` + доля 5xx на `/status`,`/queue`),
|
||||
`classify` (HEALTHY|DEGRADED — главный предмет юнит-тестов), `decide_action`,
|
||||
sentinel-state, `write_post_deploy_log`.
|
||||
- **Пороги (BR-3):** `DEGRADED` ⇔ `≥ post_deploy_fail_threshold` ПОСЛЕДОВАТЕЛЬНЫХ провалов
|
||||
health ИЛИ доля 5xx `> post_deploy_5xx_threshold`; одиночный глюк → HEALTHY (нет ложных
|
||||
откатов).
|
||||
- **Реакция:** self-hosting (`orchestrator`) — ВСЕГДА `ALERT_ONLY` (Telegram+Plane, ручной
|
||||
approve; тик НИКОГДА не откатывает/рестартит прод-контейнер); не-self +
|
||||
`post_deploy_auto_rollback=true` → хук `--rollback` (`0→ROLLBACK_OK`,
|
||||
`1/2→ROLLBACK_FAILED`+алерт); дефолт → `ALERT_ONLY`.
|
||||
- **Артефакт** `16-post-deploy-log.md` (YAML-frontmatter `post_deploy_status`/
|
||||
`action_taken`/…) — машиночитаемо для петли уроков ORCH-8; best-effort.
|
||||
- **Наблюдаемость** — блок `post_deploy` в `GET /queue` (образец `reconcile`).
|
||||
- **Инварианты:** `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`, terminal-sync,
|
||||
merge-gate, exit-коды хука (0/1/2), схема БД — НЕ меняются. Restart-safe (sentinel
|
||||
`.post-deploy-state-<repo>/<wi>/` + jobs-очередь). Kill-switch
|
||||
`post_deploy_monitor_enabled`, область `post_deploy_repos` (пусто → self-hosting).
|
||||
Условность как ORCH-35/36/43/58.
|
||||
|
||||
Подробнее: [adr-0010](adr/adr-0010-post-deploy-monitor.md), детально —
|
||||
`docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md`.
|
||||
|
||||
### Свежесть артефакта BUILD-ONCE: провенанс staging-образа (ORCH-058 — реализовано)
|
||||
BUILD-ONCE retag (ORCH-36) промоутит `SOURCE_IMAGE=orchestrator-orchestrator-staging` в прод
|
||||
**без rebuild**, полагаясь на «staging-образ свеж и провалидирован». Этой гарантии нет:
|
||||
конвейер нигде не пересобирает staging-образ из провалидированного коммита → retag мог тихо
|
||||
промоутнуть УСТАРЕВШИЙ образ (инцидент LESSONS_ORCH-036 п.4 — зелёный деплой молча
|
||||
откатывал прод). ORCH-058 обеспечивает инвариант `INV-FRESH` **двумя слоями** (defense in
|
||||
depth), только для self-hosting:
|
||||
- **A — пересборка (liveness):** детерминированный QG-под-чек `check_staging_image_fresh` на
|
||||
ребре `deploy-staging → deploy` ПОСЛЕ merge-gate и ДО Phase A пересобирает
|
||||
`orchestrator-orchestrator-staging` из worktree валидированного коммита
|
||||
(`--build-arg GIT_SHA=<sha>`, OCI-лейбл `org.opencontainers.image.revision`), пересоздаёт
|
||||
8501 и прогоняет `staging_check` против свежего образа → валидируем и промоутим один
|
||||
артефакт. FAIL → откат на `development` (как merge-gate). Сборки/recreate — ТОЛЬКО staging.
|
||||
- **B — fail-closed guard (safety):** хук шагом 2b ПЕРЕД `docker tag` сверяет лейбл `revision`
|
||||
у `SOURCE_IMAGE` с `EXPECTED_REVISION` (пробрасывает `build_deploy_command`). Несовпадение
|
||||
/ пустой лейбл / пустой ожидаемый SHA / ошибка inspect → `exit 1` → FAILED (БАГ-8 откат),
|
||||
прод не трогается. Делает тихий промоут устаревшего образа структурно невозможным даже при
|
||||
отключённой/проигравшей гонку A.
|
||||
|
||||
Якорь «провалидированного коммита» — `git rev-parse HEAD` worktree ПОСЛЕ merge-gate (один
|
||||
helper `validated_revision` питает и штамп A, и `EXPECTED_REVISION` B). Единый kill-switch
|
||||
`image_freshness_enabled` включает A+B **как целое** (нет «B без A» = вечного fail-fast);
|
||||
`image_freshness_repos` (пусто → self-hosting). `STAGE_TRANSITIONS`, exit-code хука (0/1/2),
|
||||
`check_deploy_status`, БАГ-8, merge-gate, схема БД — НЕ меняются (под-гейт ребра + лейбл
|
||||
образа, без миграций). Подробнее: [adr-0008](adr/adr-0008-staging-image-provenance.md),
|
||||
детально — `docs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md`.
|
||||
|
||||
### Reconciler: реконсиляция потерянных webhook (ORCH-053 — реализовано)
|
||||
Конвейер продвигается только входящими webhook; потерянное событие (502 на ребилде,
|
||||
нет ретраев у Plane/Gitea, неразрезолвленный `sha→branch`) → задача застревает молча
|
||||
@@ -90,6 +165,13 @@ sentinel-файлы (`<repos_dir>/.deploy-state-<repo>/<wi>/`), без мигр
|
||||
`age(updated_at) ≥ grace_for_stage(stage)` — read-only пред-оценка канонического QG;
|
||||
зелёный → `stage_engine.advance_stage(..., finished_agent=None)`; красный →
|
||||
тишина (спам нотификаций структурно невозможен). `analysis` не реконсилируется.
|
||||
**Skip escalated / Blocked / Needs-Input (ORCH-060):** ДО оценки гейта F-1
|
||||
пропускает (молча, без advance/нотификаций) задачи, которые ждут человека —
|
||||
(1) исчерпавшие лимит developer-ретраев (`developer_retry_count(task_id) >=
|
||||
MAX_DEVELOPER_RETRIES`, детерминированно, без сети — закрывает bounce-петлю
|
||||
ET-013) и (2) в явном Plane-статусе **Blocked** / **Needs Input** (Вариант A —
|
||||
запрос Plane API, без миграции БД; never-raise → консервативный skip). Гард
|
||||
retry-count проверяется первым (дёшево, локальный SQL).
|
||||
- **F-2 plane-side:** опрос Plane API per-project → `handle_status_start` /
|
||||
`handle_verdict` из `webhooks/plane.py` (логика не дублируется).
|
||||
- **F-3:** усиление `sha→branch` в `handle_ci_status` (БД-fallback по единственной
|
||||
@@ -109,6 +191,104 @@ never-raise на единицу работы; тишина при синхрон
|
||||
и реестры (`STAGE_TRANSITIONS`/`QG_CHECKS`) не меняются. Подробнее:
|
||||
[adr-0007](adr/adr-0007-reconciler.md), детально — `docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md`.
|
||||
|
||||
### Job-reaper + проактивный реклейм merge-lease (ORCH-065 — design)
|
||||
Финализация статуса job (`done`/`queued`/`failed`) выполняется ТОЛЬКО в
|
||||
`launcher._monitor_agent → _finalize_job` внутри живого процесса. Смерть
|
||||
monitor-потока/процесса между `proc.wait()` и `_finalize_job` (краш, OOM,
|
||||
self-restart во время deploy) оставляла строку `jobs` навсегда `running`; при
|
||||
`max_concurrency=1` одна зомби-строка блокирует claim всех job → встаёт конвейер
|
||||
ВСЕХ проектов (инциденты 07.06: jobs 236/239/242/254). `requeue_running_jobs()`
|
||||
спасал ТОЛЬКО на старте процесса. Симметрично залипал merge-lease (ORCH-043):
|
||||
реклейм был лениво-по-TTL и только при чужом `acquire`, liveness держателя по pid
|
||||
не проверялся. Это последняя ручная точка автономного self-deploy (блокер ORCH-54).
|
||||
ORCH-065 вводит фоновый watchdog, чтобы смерть процесса/потока на любой стадии НЕ
|
||||
оставляла навсегда захваченных ресурсов:
|
||||
- **Job-reaper** (`src/job_reaper.py`) — daemon-поток по образцу `reconciler`,
|
||||
работает **без рестарта**. Трёхуровневая liveness: Tier-1 мёртвый `jobs.pid`
|
||||
(новая колонка) после `reaper_dead_ticks` подряд тиков (анти-ложноположительность
|
||||
— живой долгий агент не реапится); Tier-2 `agent_runs.exit_code` записан, а job
|
||||
ещё `running` — но это окно неоднозначно (живой monitor пишет exit_code ПЕРВЫМ,
|
||||
затем git push/PR/Plane-комментарии), поэтому Tier-2 реапит только после
|
||||
finalization-grace `reaper_finalize_grace_s` (живой финализирующий monitor НЕ
|
||||
реапится); Tier-3 backstop по потолку `reaper_max_running_s` (> max
|
||||
agent_timeout+grace). Действие переиспользует контракты по принципу
|
||||
**claim-before-act**: для exit0 канонический QG оценивается read-only ПЕРЕД
|
||||
атомарным claim, затем claim `done` ПЕРВЫМ и только победитель claim делает
|
||||
`_try_advance_stage` (advance+enqueue) — проигравший claim (поздний monitor /
|
||||
стартовый requeue) не выполняет побочных эффектов (нет дубль-advance/-enqueue);
|
||||
источник истины — канонический QG, не факт «exit0»; гейт красный или exit≠0/
|
||||
неизвестно → `attempts<max`→`queued`, иначе `failed`+Telegram. Атомарный
|
||||
reap-claim (`UPDATE ... WHERE id=? AND status='running'`) совместим со стартовым
|
||||
`requeue_running_jobs` (restart-safe, без двойной обработки).
|
||||
- **Проактивный реклейм stale/dead lease** (функции в `merge_gate.py`:
|
||||
`pid_alive`, `reclaim_stale_lease`) — на старте (рядом с `requeue_running_jobs`)
|
||||
и периодически из тика reaper: освобождает lease, чей держатель **мёртв** (pid
|
||||
не жив) ИЛИ **просрочен** (TTL `merge_lock_timeout_s`); живой держатель в
|
||||
пределах TTL — НЕ трогать (защита легитимного merge). holder-aware, never-raise,
|
||||
условность как ORCH-43 (`merge_gate_repos`/self-hosting).
|
||||
- **Идемпотентная финализация merge** — без новой merge-логики: re-drive через
|
||||
reaper→`queued`→переисполнение стадии / reconciler; дорогие шаги не повторяются
|
||||
(`branch_is_behind_main==False`); добавлен never-raise guard `pr_already_merged`
|
||||
(читает состояние PR) — уже слит = no-op. **Консультируется самим merge-актором:**
|
||||
фактический merge PR в `main` делает агент `deployer` (в начале стадии `deploy`),
|
||||
поэтому wiring — в его промпте `.openclaw/agents/deployer.md`, который вызывает
|
||||
`pr_already_merged` ПЕРЕД любым (повторным) merge (AC-11). Чек `check_branch_mergeable`
|
||||
НЕ меняется (AC-13): он на ПЕРВОМ ребре `deploy-staging → deploy`, а риск второго
|
||||
merge — на re-drive самой стадии `deploy`.
|
||||
- **Схема БД:** единственное изменение — `jobs.pid INTEGER` через идемпотентный
|
||||
`_ensure_column` (live-safe). `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, БАГ-8,
|
||||
exit-коды хука, файл-схема lease — без изменений.
|
||||
- **Наблюдаемость:** блок `reaper` в `GET /queue` (enabled, interval, last_run_ts,
|
||||
reaped_total, last_reaped, lease_reclaimed_total); каждый reap/lease-reclaim →
|
||||
`logger.warning`; reap→`failed` и lease-reclaim → Telegram.
|
||||
- **Kill-switch'и:** `ORCH_REAPER_ENABLED`, `ORCH_REAPER_INTERVAL_S`,
|
||||
`ORCH_REAPER_DEAD_TICKS`, `ORCH_REAPER_MAX_RUNNING_S`,
|
||||
`ORCH_REAPER_FINALIZE_GRACE_S`, `ORCH_LEASE_RECLAIM_ENABLED`; `false` → строго
|
||||
прежнее поведение.
|
||||
|
||||
Подробнее: [adr-0011](adr/adr-0011-job-reaper-lease-reclaim.md), детально —
|
||||
`docs/work-items/ORCH-065/06-adr/ADR-001-job-reaper-and-lease-reclaim.md`.
|
||||
|
||||
### Осмысленная статусная модель Plane (ORCH-066 — реализовано)
|
||||
Plane-доска была семантически перегружена: `In Progress` означал «человек запускает
|
||||
конвейер», «идёт анализ», «идёт прод-деплой» и «возврат из Needs Input» одновременно.
|
||||
ORCH-066 наводит порядок по утверждённой Owner модели, меняя **только слой B**
|
||||
(Plane-индикация: `src/plane_sync.py` + точки простановки в `src/stage_engine.py`/
|
||||
`src/webhooks/plane.py`/`src/reconciler.py`) и **не трогая слой A** (`STAGE_TRANSITIONS`,
|
||||
инвариант). Статус — индикация, не управление (вердикты по-прежнему из YAML-frontmatter):
|
||||
```
|
||||
Backlog → Todo → [To Analyse] → Analysis → [In Review → Approved] → Architecture →
|
||||
Development → Code-Review → Testing → Awaiting Deploy → [Confirm Deploy] → Deploying →
|
||||
Monitoring after Deploy → Done
|
||||
```
|
||||
`[...]` = человеческий вход-триггер; остальное ставит орк.
|
||||
- **6 новых логических ключей** (`to_analyse`/`analysis`/`code_review`/`awaiting_deploy`/
|
||||
`deploying`/`monitoring`) в `_PLANE_NAME_TO_KEY` (резолв по имени) + `_DEFAULT_STATES`.
|
||||
`To Analyse` заменяет `In Progress` как вход-триггер (старт + resume аналитика из Needs
|
||||
Input; fork «старт vs resume» по `get_task_by_plane_id`+`has_active_job_for_task` —
|
||||
сохранён). Стадии: analysis→`Analysis`, review→`Code-Review` (`_STAGE_TO_STATE_KEY`).
|
||||
- **Self-deploy фазы:** Phase A → `Awaiting Deploy` (разгружает `In Review`), Phase B →
|
||||
`Deploying`, Phase C/terminal-sync (self) → `Monitoring after Deploy` (НЕ `Done` сразу);
|
||||
post-deploy monitor (ORCH-021): HEALTHY-окно → `Done`, DEGRADED → `Blocked` (тик
|
||||
по-прежнему НИКОГДА не рестартит прод — ALERT_ONLY). Не-self репо: `deploy → Done` как
|
||||
сейчас (terminal-sync разводится по `post_deploy.post_deploy_applies`).
|
||||
- **Fail-closed (project-relative alias-fallback):** отсутствующий новый статус в проекте
|
||||
деградирует на **собственный базовый UUID того же проекта** (`to_analyse/analysis→in_progress`,
|
||||
`code_review→review`, `awaiting_deploy→in_review`, `deploying→in_progress`,
|
||||
`monitoring→done`) — индикация откатывается к текущей, конвейер не ломается, PATCH валиден
|
||||
даже при частичной конфигурации. Enduro (статусы не создаются) → строго прежнее поведение.
|
||||
Усиленный паттерн ORCH-059 AC-7.
|
||||
- **Reconciler:** F-2 триггер `in_progress`→`to_analyse`; Guard 2 skip-set расширен
|
||||
активными ожиданиями (`awaiting_deploy`/`deploying`/`monitoring`) с **вычитанием базовых
|
||||
рабочих статусов** — на enduro (алиасы схлопнуты) нулевой регресс, на orchestrator skip
|
||||
реальных ожиданий (BR-13).
|
||||
- **Инварианты:** `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`, exit-коды хука,
|
||||
merge-gate, `Confirm Deploy`, механизм `Needs Input` (analyst-only), схема БД — без
|
||||
изменений. Без нового kill-switch (раскат гейтится созданием Plane-статусов оператором).
|
||||
Инфра-предусловие — `docs/work-items/ORCH-066/07-infra-requirements.md`.
|
||||
|
||||
Подробнее: `docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md`.
|
||||
|
||||
## Откаты
|
||||
- Reviewer REQUEST_CHANGES → откат на `development` + retry (`MAX_DEVELOPER_RETRIES = 3`).
|
||||
- Tester `check_tests_passed` FAIL → откат на `development` + retry.
|
||||
@@ -142,7 +322,7 @@ never-raise на единицу работы; тишина при синхрон
|
||||
- `events` — входящие вебхуки (дедуп)
|
||||
- `tasks` — задачи и их стадии
|
||||
- `agent_runs` — запуски агентов (run_id, usage, cost)
|
||||
- `jobs` — очередь задач (ORCH-1)
|
||||
- `jobs` — очередь задач (ORCH-1); колонка `pid` (ORCH-065) — pid агентского процесса для liveness-детекции зомби job-reaper'ом
|
||||
|
||||
## Изоляция (git worktree, ORCH-2)
|
||||
Каждая задача исполняется в отдельном git worktree, ветки не пересекаются. Репозитории проектов разделены под `/repos/<project>`.
|
||||
@@ -152,7 +332,7 @@ never-raise на единицу работы; тишина при синхрон
|
||||
|--------|------|----------|
|
||||
| GET | `/health` | health check |
|
||||
| GET | `/status` | активные задачи (stage != done) |
|
||||
| GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + последние jobs |
|
||||
| GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + reaper (ORCH-065) + post_deploy (ORCH-021) + последние jobs |
|
||||
| POST | `/webhook/plane` | Plane webhook |
|
||||
| POST | `/webhook/gitea` | Gitea webhook (push, PR, CI status) |
|
||||
|
||||
@@ -166,5 +346,4 @@ never-raise на единицу работы; тишина при синхрон
|
||||
Схема БД, потоки данных, resilience-слой, детали Dockerfile — [internals.md](internals.md).
|
||||
|
||||
---
|
||||
*Актуально на 2026-06-06. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. ORCH-043: merge-gate — design (см. adr-0006), реализация в ветке feature/ORCH-043. ORCH-036: исполняемый самодеплой стадии `deploy` — design (см. adr-0007), реализация в ветке feature/ORCH-036.*
|
||||
*Актуально на 2026-06-06. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. ORCH-043: merge-gate — design (см. adr-0006), реализация в ветке feature/ORCH-043. ORCH-053: reconciler — реализовано (см. adr-0007, src/reconciler.py).*
|
||||
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-066 (осмысленная статусная модель Plane — слой B, `docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md`) — реализовано в ветке feature/ORCH-066-plane (только Plane-индикация: новые ключи `to_analyse`/`analysis`/`code_review`/`awaiting_deploy`/`deploying`/`monitoring` в `_PLANE_NAME_TO_KEY`/`_DEFAULT_STATES` + project-relative `_STATE_ALIAS_FALLBACK` в get_project_states + `_STAGE_TO_STATE_KEY` analysis/review + 5 новых `set_issue_*` в src/plane_sync.py; триггер `in_progress`→`to_analyse` и `set_issue_analysis` в src/webhooks/plane.py; Phase A→Awaiting Deploy / Phase B→Deploying / terminal-sync split monitoring↔done / post-deploy monitor HEALTHY→Done DEGRADED→Blocked в src/stage_engine.py; F-2 триггер `to_analyse` + Guard 2 skip-set с вычитанием base_working в src/reconciler.py; `STAGE_TRANSITIONS`/QG/схема БД НЕ трогаются; без kill-switch — раскат гейтится созданием 6 Plane-статусов оператором, `docs/work-items/ORCH-066/07-infra-requirements.md`; обновлять при изменении этих мест).*
|
||||
|
||||
@@ -12,6 +12,16 @@ Per-work-item решения живут в `docs/work-items/<id>/06-adr/ADR-NNN-
|
||||
| adr-0005 | Контейнеры бегут под uid:gid хоста (1000:1000) | accepted | 2026-06-06 | ORCH-040 |
|
||||
| adr-0006 | Merge-gate (догон main + re-test + сериализация слияний) | proposed | 2026-06-06 | ORCH-043 |
|
||||
| adr-0007 | Reconciler застрявших стадий (sweeper потерянных webhook) | accepted | 2026-06-06 | ORCH-053 |
|
||||
| adr-0007 | Исполняемый самодеплой стадии `deploy` (файл adr-0007-executable-self-deploy) | accepted | 2026-06-06 | ORCH-036 |
|
||||
| adr-0008 | Провенанс staging-образа перед BUILD-ONCE retag | accepted | 2026-06-06 | ORCH-058 |
|
||||
| adr-0009 | Толерантность staging-вердикта к инфраструктурным FAIL | accepted | 2026-06-07 | ORCH-061 |
|
||||
| adr-0010 | Post-deploy мониторинг прода + реакция на деградацию | proposed | 2026-06-07 | ORCH-021 |
|
||||
| adr-0011 | Job-reaper + проактивный реклейм merge-lease | accepted | 2026-06-07 | ORCH-065 |
|
||||
|
||||
> ⚠️ Историческая коллизия: номер `0007` занят двумя файлами —
|
||||
> `adr-0007-reconciler.md` (ORCH-053) и `adr-0007-executable-self-deploy.md`
|
||||
> (ORCH-036). Оба accepted; для новых сквозных ADR использовать следующий
|
||||
> свободный номер (текущий максимум — `0011`).
|
||||
|
||||
## Формат
|
||||
**Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded.
|
||||
|
||||
@@ -61,6 +61,14 @@ grace + `max_concurrency=1`); never-raise на единицу работы; ти
|
||||
(`reconcile_plane_enabled` гасит только F-2); reconciler не рестартит/не роняет
|
||||
прод-контейнер. БД-схема и реестры (`STAGE_TRANSITIONS`/`QG_CHECKS`) не меняются.
|
||||
|
||||
## Уточнения
|
||||
- **ORCH-060** (`docs/work-items/ORCH-060/06-adr/ADR-001-reconciler-skip-escalated.md`):
|
||||
F-1 (`_reconcile_gate_task`) приобретает два пред-гарда ДО оценки гейта —
|
||||
пропускает escalated (`developer_retry_count ≥ MAX_DEVELOPER_RETRIES`,
|
||||
детерминированно) и Blocked/Needs-Input (Вариант A, Plane API, без миграции)
|
||||
задачи. Инварианты adr-0007 сохранены (схема/реестры не меняются, never-raise,
|
||||
тишина при пропуске).
|
||||
|
||||
## Связи
|
||||
adr-0002 (очередь / `available_at`, single-process-singleton), adr-0003 (условный
|
||||
гейт — образец условности/флагов раската), adr-0006 (merge-gate как под-гейт ребра
|
||||
|
||||
77
docs/architecture/adr/adr-0008-staging-image-provenance.md
Normal file
77
docs/architecture/adr/adr-0008-staging-image-provenance.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# ADR-0008: Провенанс staging-образа перед BUILD-ONCE retag в прод (ORCH-058)
|
||||
|
||||
## Статус
|
||||
Accepted (design) — реализация в ветке `feature/ORCH-058-self-deploy-retag-staging`.
|
||||
Метка: `arch:major-change`.
|
||||
|
||||
> Примечание о нумерации: в `adr/` исторически два файла `adr-0007-*`
|
||||
> (`executable-self-deploy`, `reconciler`) — пред-существующая коллизия. Этот ADR берёт
|
||||
> следующий свободный номер **0008**; коллизию 0007 не трогаем (вне объёма ORCH-058).
|
||||
|
||||
## Контекст
|
||||
|
||||
ORCH-36 (`adr-0007-executable-self-deploy`) сделал стадию `deploy` исполняемой для
|
||||
self-hosting: Phase B запускает host-хук, который шагом **2b** (BUILD-ONCE) делает
|
||||
`docker tag $SOURCE_IMAGE → $TARGET_IMAGE` **без rebuild** — «прод = ровно тот артефакт,
|
||||
что прошёл staging». Предпосылка: staging-образ свеж и собран из провалидированного кода.
|
||||
|
||||
**Этой гарантии нет.** Конвейер нигде не пересобирает `orchestrator-orchestrator-staging`
|
||||
из провалидированного коммита; `deploy-staging` лишь гоняет `staging_check.py` против уже
|
||||
работающего 8501. Инцидент (LESSONS_ORCH-036 п.4): staging-образ не пересобрали → проверка
|
||||
прошла против старого кода → retag промоутнул СТАРЫЙ образ → прод **молча** откатился на
|
||||
2-дневный код. Зелёный гейт = ложный позитив. Самый опасный из 4 багов: не падает, а тихо
|
||||
откатывает инструмент, обслуживающий все проекты.
|
||||
|
||||
## Решение
|
||||
|
||||
Гарантировать `INV-FRESH`: в прод промоутится только образ, собранный из коммита,
|
||||
провалидированного `deploy-staging` для данной задачи; иначе fail-fast (`FAILED` → откат на
|
||||
`development`, БАГ-8), прод не трогается. Достигается **двумя взаимодополняющими слоями**
|
||||
(defense in depth), только для self-hosting (условность как ORCH-35/36/43):
|
||||
|
||||
- **A — пересборка (liveness).** На ребре `deploy-staging → deploy`, ПОСЛЕ merge-gate и ДО
|
||||
Phase A, детерминированный QG-под-чек `check_staging_image_fresh` пересобирает
|
||||
`orchestrator-orchestrator-staging` из worktree валидированного коммита
|
||||
(`--build-arg GIT_SHA=<sha>`, лейбл `org.opencontainers.image.revision`), пересоздаёт 8501
|
||||
и прогоняет `staging_check`. FAIL → откат на `development`. Так валидируемый и промоутимый
|
||||
артефакт — один и тот же; гарантирует наличие зелёного пути (нет вечного fail-fast).
|
||||
- **B — fail-closed guard (safety).** Хук шагом 2b ПЕРЕД `docker tag` сверяет лейбл
|
||||
`revision` образа `SOURCE_IMAGE` с `EXPECTED_REVISION` (пробрасывает `build_deploy_command`).
|
||||
Несовпадение / пустой лейбл / пустой ожидаемый SHA / ошибка inspect → `exit 1` → FAILED.
|
||||
Делает тихий промоут устаревшего образа структурно невозможным даже при отключённой/
|
||||
проигравшей гонку A.
|
||||
|
||||
**Якорь провалидированного коммита** — `git rev-parse HEAD` в worktree ПОСЛЕ merge-gate
|
||||
(post-rebase tree, который ре-тестирован и сольётся в `main`). Один helper
|
||||
`validated_revision(repo, branch)` питает и штамп сборки (A), и `EXPECTED_REVISION` (B).
|
||||
|
||||
**Условность и kill-switch:** единый `image_freshness_enabled` (вкл/выкл A+B как целое,
|
||||
чтобы не было «B без A» = вечный fail-fast), `image_freshness_repos` (CSV; пусто →
|
||||
self-hosting). Все настройки с префиксом `ORCH_`.
|
||||
|
||||
### Что НЕ меняется
|
||||
`STAGE_TRANSITIONS` (набор стадий — под-гейт ребра, не стадия), exit-code хука (0/1/2),
|
||||
`map_exit_code_to_status`, `check_deploy_status`/`_parse_deploy_status`, БАГ-8, terminal-sync,
|
||||
merge-gate, Phase A/B/C. Схема БД — без миграций (провенанс в лейбле образа, не в БД).
|
||||
|
||||
### Что добавляется (сквозное)
|
||||
- QG `check_staging_image_fresh` в реестре `QG_CHECKS` (+ snapshot-тест), wired через
|
||||
`_handle_image_freshness` в `stage_engine` (рядом с merge-gate).
|
||||
- Режим хука `--build-staging` (build из worktree + recreate 8501; STAGING-safe дефолты).
|
||||
- OCI-лейбл `org.opencontainers.image.revision` в `Dockerfile` (`ARG GIT_SHA`).
|
||||
- Helpers `validated_revision` / `rebuild_staging_image` в `self_deploy.py` (never-raise).
|
||||
|
||||
## Последствия
|
||||
|
||||
- Класс «тихого регресса прод» закрыт структурно (B); валидный деплой всегда доходит до
|
||||
зелёного (A) — устранён ручной bootstrap-разрыв пересборки staging.
|
||||
- Латентность ребра растёт (build + recreate + повторный staging_check); `staging_check`
|
||||
гоняется дважды (soft pre-check агента + авторитетный код) — плата за «валидируем =
|
||||
промоутим».
|
||||
- Все сборки/recreate — ТОЛЬКО staging (8501); прод (8500) не трогается; `main` не пушится.
|
||||
Новая под-компонента → `arch:major-change`.
|
||||
|
||||
## Связанные ADR
|
||||
`adr-0007-executable-self-deploy` (BUILD-ONCE, Phase A/B/C), `adr-0006-merge-gate` (образец
|
||||
edge-под-гейта), `adr-0003-staging-gate` (условность self-hosting), `adr-0005`
|
||||
(run-as-host-uid). Детальный per-work-item: `docs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md`.
|
||||
56
docs/architecture/adr/adr-0009-staging-infra-tolerance.md
Normal file
56
docs/architecture/adr/adr-0009-staging-infra-tolerance.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# adr-0009: Толерантность staging-вердикта к заведомо инфраструктурным FAIL
|
||||
|
||||
- **Статус:** accepted
|
||||
- **Дата:** 2026-06-07
|
||||
- **Задача:** ORCH-061
|
||||
- **Детально:** `docs/work-items/ORCH-061/06-adr/ADR-001-staging-infra-tolerance.md`
|
||||
|
||||
## Контекст
|
||||
Self-hosting `orchestrator` зацикливался на `deploy-staging`: `staging_check.py`
|
||||
давал 2 ложных FAIL (C9a — ветка в sandbox, C9b — analyst-job в очереди), вызванных
|
||||
отсутствием sandbox-настроек (bot-аккаунты не члены SANDBOX-проекта), а не регрессом
|
||||
кода. `staging_check.py` делал `sys.exit(1)` при любом FAIL → deployer писал
|
||||
`staging_status: FAILED` → `check_staging_status` FAILED → откат `deploy-staging →
|
||||
development` → петля (жгла developer-ретраи и кредиты). Прод-деплой орка приходилось
|
||||
доводить вручную — блокер автономного внедрения (ORCH-54).
|
||||
|
||||
## Решение
|
||||
Классифицировать проверки staging-suite на **REAL** (pipeline) и **SANDBOX_INFRA**
|
||||
(заведомо инфраструктурные, узкий allowlist `{C9a, C9b}`) и сделать вердикт
|
||||
толерантным к инфра-FAIL, сохранив fail-closed для реальных проверок:
|
||||
|
||||
- Новый leaf-модуль `src/staging_verdict.py` (pure, never-raise, stdlib):
|
||||
`classify_check(label)` + `compute_staging_verdict(items, infra_tolerant)`.
|
||||
Правило: упала хоть одна REAL → FAILED/exit1; упали ТОЛЬКО SANDBOX_INFRA и
|
||||
толерантность вкл → SUCCESS/exit0 (waived); толерантность выкл → legacy strict
|
||||
(любой FAIL → FAILED).
|
||||
- `scripts/staging_check.py` помечает проверки категориями, считает вердикт через
|
||||
`staging_verdict`, печатает `INFRA-WAIVED` при вайвере (наблюдаемость).
|
||||
- Kill-switch `staging_infra_tolerance_enabled` (env
|
||||
`ORCH_STAGING_INFRA_TOLERANCE_ENABLED`, дефолт `True`; в `.env.staging`).
|
||||
- `check_staging_status` / `_parse_staging_status` / `STAGE_TRANSITIONS` / реестр
|
||||
`QG_CHECKS` — **без изменений**; новый QG-чек не вводится. Условность ORCH-35
|
||||
сохранена (не-self → no-op N/A).
|
||||
- Инвариант FR-3: «no changes to commit» на action-стадиях (`deploy-staging`/`deploy`)
|
||||
не есть недовыполнение — продвижение определяется exit0 + гейт-вердиктом
|
||||
(launcher уже не откатывает; добавлена observability-строка).
|
||||
|
||||
## Альтернативы
|
||||
- Только починить sandbox-инфру (направление а) — хрупко, не структурно, вне
|
||||
автономной досягаемости таска; оставлено как опциональное hardening.
|
||||
- «Зелёный по умолчанию» при недоступности проверок — запрещён (fail-closed).
|
||||
- Новый QG-чек / структурный артефакт `15-staging-log.md` — избыточно, меняло бы
|
||||
контракты/реестр; толерантность размещена в suite до артефакта.
|
||||
|
||||
## Последствия
|
||||
- Петля устранена; страховка цела (реальный регресс → FAILED → откат).
|
||||
- Чистая вердикт-логика юнит-тестируема без live staging/docker.
|
||||
- Контракты гейтов/стадий/вердиктов/реестра и схема БД неизменны.
|
||||
- Риск: узкое окно — реальный регресс именно в создании ветки/постановке
|
||||
analyst-job может быть заваивен; митигировано allowlist'ом `{C9a,C9b}` + условием
|
||||
«все REAL (вкл. C7/C8) зелёные» + INFRA-WAIVED-логом. Разблокирует ORCH-54.
|
||||
|
||||
## Связи
|
||||
adr-0003 (условный staging-гейт — база `is_self_hosting_repo` / `check_staging_status`),
|
||||
adr-0006 (merge-gate), adr-0007 (исполняемый self-deploy), adr-0008 (провенанс
|
||||
staging-образа). Блокирует ORCH-54.
|
||||
85
docs/architecture/adr/adr-0010-post-deploy-monitor.md
Normal file
85
docs/architecture/adr/adr-0010-post-deploy-monitor.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# adr-0010: Post-deploy мониторинг прода + реакция на деградацию
|
||||
|
||||
- **Статус:** proposed (design) — реализация в ветке `feature/ORCH-021-post-deploy-rollback`
|
||||
- **Дата:** 2026-06-07
|
||||
- **Задача:** ORCH-021
|
||||
- **Метка:** `arch:major-change` (новая под-компонента + новый reserved-agent job-kind)
|
||||
- **Детальный ADR:** `docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md`
|
||||
|
||||
## Контекст
|
||||
Конвейер заканчивается на `deploy → done`: `check_deploy_status` видит
|
||||
`deploy_status: SUCCESS` → terminal-sync (Plane → Done, release merge-lease), и
|
||||
оркестратор **забывает про прод**. «Успех» сегодня = health-check в момент рестарта
|
||||
(~60с окно в `orchestrator-deploy-hook.sh`). Класс инцидентов «зелёный деплой, красный
|
||||
прод» (прецедент **ET-8**): деградация проявляется через минуты под боевым трафиком,
|
||||
health отвечает `200 ok`, фича сломана. Для self-hosting опасно вдвойне — сломанный
|
||||
прод-орк (8500) обслуживает ВСЕ проекты из общего инстанса.
|
||||
|
||||
## Решение
|
||||
Продлить ответственность конвейера **ЗА** `done`: после терминального перехода для
|
||||
применимого репо армится пост-деплой наблюдение окна `post_deploy_window_s` (дефолт
|
||||
~15 мин) с интервалом `post_deploy_interval_s`; деградация фиксируется по
|
||||
**детерминированным порогам**, при подтверждении выполняется реакция.
|
||||
|
||||
**Механизм — reserved-agent job `post-deploy-monitor`** (калька `deploy-finalizer`,
|
||||
ORCH-36), НЕ отдельная стадия и НЕ daemon-поток:
|
||||
- **Арм:** в `stage_engine.advance_stage`, в блоке `next_stage == "done"`, при
|
||||
`post_deploy.post_deploy_applies(repo)` → `post_deploy.arm_monitor(...)` (sentinel
|
||||
`armed` = идемпотентность, первый job через `enqueue_job(available_at_delay_s=...)`).
|
||||
- **Тик:** `launcher.launch_job` перехватывает `agent == "post-deploy-monitor"` ДО
|
||||
`_spawn` → `stage_engine.run_post_deploy_monitor(job)`: один опрос сигналов, append в
|
||||
персистентный `series`, классификация; HEALTHY и окно не истекло → перепостановка с
|
||||
задержкой; иначе → реакция + артефакт + `mark_done`.
|
||||
- **Чистая логика — новый leaf-модуль `src/post_deploy.py`** (never-raise, по образцу
|
||||
`self_deploy.py`/`staging_verdict.py`): `post_deploy_applies`, `probe_signals`
|
||||
(опрос `/health` + доля 5xx на `/status`,`/queue`), `classify` (HEALTHY|DEGRADED —
|
||||
главный предмет юнит-тестов), `decide_action` (NONE|ROLLBACK|ALERT_ONLY с учётом
|
||||
self-hosting), sentinel-state хелперы, `write_post_deploy_log`.
|
||||
|
||||
**Сигналы и пороги (детерминированно, AC-3…AC-6):** `DEGRADED` ⇔ `≥
|
||||
post_deploy_fail_threshold` ПОСЛЕДОВАТЕЛЬНЫХ провалов health ИЛИ доля 5xx на окне `>
|
||||
post_deploy_5xx_threshold`. Одиночный глюк < порога → HEALTHY (нет ложных откатов).
|
||||
|
||||
**Реакция (BR-4/BR-5):**
|
||||
- **Self-hosting (`orchestrator`) — ВСЕГДА `ALERT_ONLY`:** громкий Telegram + Plane,
|
||||
запрос ручного approve отката. Тик НИКОГДА не откатывает/рестартит прод-контейнер
|
||||
(структурный инвариант). Откат прод-орка, если оператор решит, — только detached
|
||||
host-процесс (`self_deploy.initiate_deploy`), вне тика (MVP).
|
||||
- **Не-self + `post_deploy_auto_rollback=True`:** хук `--rollback` с прод-env; exit
|
||||
`0 → ROLLBACK_OK`, `1/2 → ROLLBACK_FAILED` + громкий алерт.
|
||||
- Дефолт (`auto_rollback=False`) → `ALERT_ONLY`.
|
||||
|
||||
**Артефакт `16-post-deploy-log.md`** (новый) с YAML-frontmatter (`post_deploy_status`,
|
||||
`action_taken`, `window_s`, `checks_total/failed`) — машиночитаемо для петли уроков
|
||||
ORCH-8; best-effort. **Наблюдаемость** — блок `post_deploy` в `GET /queue` (образец
|
||||
`reconcile.status()`).
|
||||
|
||||
## Альтернативы
|
||||
- **Daemon-watchdog (как reconciler)** — отклонён: per-task серия опросов в памяти не
|
||||
restart-safe (а деплой орка = рестарт); restart-safe-вариант требует тех же sentinel,
|
||||
reserved-agent проще и уже имеет проверенную jobs+sentinel машинерию.
|
||||
- **Отдельная пост-deploy стадия + QG** — отклонён: меняет `STAGE_TRANSITIONS`/
|
||||
`QG_CHECKS`, ломает семантику терминального `done`; наблюдение принципиально ПОСЛЕ
|
||||
`done`.
|
||||
- **Авто-rollback прод-орка из тика** — отклонён (self-hosting safety): групповой риск;
|
||||
контейнер не откатит себя надёжно. Self → alert + ручной approve (как ORCH-54).
|
||||
- **Колонка в `tasks`** — отклонён: миграция на проде; sentinel-файлы restart-safe
|
||||
(как ORCH-36/53/58).
|
||||
|
||||
## Последствия
|
||||
- Класс «зелёный деплой, красный прод» закрыт измеримыми порогами; деградация =
|
||||
сигнал для ORCH-8.
|
||||
- Реестры (`STAGE_TRANSITIONS`/`QG_CHECKS`), контракт `check_deploy_status`,
|
||||
terminal-sync, merge-gate, exit-code-контракт хука, схема БД — **не меняются**.
|
||||
- Дефолты безопасны: kill-switch on, auto-rollback off, self только alert.
|
||||
- Ограничение: монитор self бежит внутри наблюдаемого прода — полностью wedged
|
||||
контейнер = пропущенный тик/алерт (known MVP gap; внешний watchdog — follow-up).
|
||||
- Self-hosting: тик не рестартит/не роняет прод-контейнер; kill-switch
|
||||
`post_deploy_monitor_enabled` обязателен; поэтапный раскат через `post_deploy_repos`.
|
||||
|
||||
## Связи
|
||||
adr-0007-executable-self-deploy (ORCH-36 — sentinel/detached-host/finalizer образец,
|
||||
`map_exit_code_to_status`), adr-0007-reconciler (ORCH-53 — daemon/`status()` образец,
|
||||
отклонён как основной механизм), adr-0006 (merge-gate — условность/флаги раската),
|
||||
adr-0003 (staging-gate — образец условности), adr-0008 (provenance — `.deploy-prev-image`/
|
||||
хук-откат). Прецедент ET-8. Будущее: ORCH-8 (петля уроков), ORCH-54 (полный авто).
|
||||
82
docs/architecture/adr/adr-0011-job-reaper-lease-reclaim.md
Normal file
82
docs/architecture/adr/adr-0011-job-reaper-lease-reclaim.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# adr-0011: Job-reaper + проактивный реклейм merge-lease
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Статус | accepted |
|
||||
| Дата | 2026-06-07 |
|
||||
| Источник | ORCH-065 (BUG P0, блокер ORCH-54) |
|
||||
| Детально | `docs/work-items/ORCH-065/06-adr/ADR-001-job-reaper-and-lease-reclaim.md` |
|
||||
|
||||
## Контекст
|
||||
|
||||
Единый инстанс с общей БД и очередью (`jobs`, `max_concurrency=1` для
|
||||
self-hosting). Финализация статуса job (`done`/`queued`/`failed`) происходит
|
||||
ТОЛЬКО в `launcher._monitor_agent → _finalize_job` внутри живого процесса. Смерть
|
||||
monitor-потока/процесса между `proc.wait()` и `_finalize_job` (краш, OOM,
|
||||
self-restart во время deploy) оставляет строку `jobs` навсегда `running`. При
|
||||
`max_concurrency=1` одна такая зомби-строка блокирует claim всех job →
|
||||
**встаёт конвейер всех проектов**. Единственная защита — `requeue_running_jobs()`
|
||||
— работает ТОЛЬКО на старте процесса. Симметрично: merge-lease (ORCH-043,
|
||||
файл `.merge-lease-<repo>.json`) реклеймится лишь лениво по TTL при чужом
|
||||
`acquire`; liveness держателя по pid не проверяется → залипший lease блокирует
|
||||
чужие merge. Это последняя ручная точка автономного self-deploy (блокер ORCH-54);
|
||||
доказанные инциденты 07.06 — jobs 236/239/242/254.
|
||||
|
||||
## Решение
|
||||
|
||||
1. **Job-reaper** — новый daemon-поток `src/job_reaper.py` (каркас `reconciler`:
|
||||
never-raise, `_stop`-Event, старт/стоп в `lifespan`, снимок в `/queue`,
|
||||
kill-switch). Работает **без рестарта** процесса. Liveness — трёхуровневая:
|
||||
Tier-1 мёртвый `jobs.pid` (новая колонка) после `reaper_dead_ticks` подряд
|
||||
тиков; Tier-2 `agent_runs.exit_code` записан, а job ещё `running` — но только
|
||||
после finalization-grace `reaper_finalize_grace_s` (окно неоднозначно: живой
|
||||
monitor пишет exit_code ПЕРВЫМ, затем git push/PR/Plane-комментарии, поэтому
|
||||
живой финализирующий monitor НЕ реапится); Tier-3 backstop по потолку
|
||||
`reaper_max_running_s`. Действие — **claim-before-act**: для exit0 канонический
|
||||
QG оценивается read-only ПЕРЕД атомарным claim, затем claim `done` ПЕРВЫМ и
|
||||
только победитель claim выполняет `_try_advance_stage` (advance+enqueue) —
|
||||
проигравший не делает побочных эффектов (источник истины — QG, не «exit0»);
|
||||
гейт красный или exit≠0 / неизвестно → `attempts<max` → `queued`, иначе
|
||||
`failed`+Telegram. Атомарный reap-claim (`UPDATE ... WHERE id=? AND
|
||||
status='running'` + `rowcount`, как `claim_next_job`) исключает двойную
|
||||
обработку (совместимость со стартовым `requeue_running_jobs`).
|
||||
2. **Проактивный реклейм stale/dead lease** — функции в `merge_gate.py`
|
||||
(`pid_alive`, `reclaim_stale_lease`), вызываемые на старте (рядом с
|
||||
`requeue_running_jobs`) и периодически из тика reaper. Освобождение, если
|
||||
держатель **мёртв** (pid не жив) ИЛИ **просрочен** (TTL); живой держатель в
|
||||
пределах TTL — НЕ трогать. holder-aware, never-raise, условность как ORCH-43.
|
||||
3. **Идемпотентная финализация merge** — без новой merge-логики: re-drive через
|
||||
reaper→`queued`→переисполнение стадии / reconciler; дорогие шаги не
|
||||
повторяются (`branch_is_behind_main==False`); добавлен детерминированный
|
||||
never-raise guard `pr_already_merged` (читает состояние PR), консультируемый
|
||||
перед повторным merge → уже слит = no-op.
|
||||
4. **Схема БД** — `jobs.pid INTEGER` через идемпотентный `_ensure_column`
|
||||
(паттерн live-safe миграции). Больше ничего не меняется.
|
||||
|
||||
Kill-switch'и (`ORCH_*`): `reaper_enabled`, `reaper_interval_s`,
|
||||
`reaper_dead_ticks`, `reaper_max_running_s`, `reaper_finalize_grace_s`,
|
||||
`lease_reclaim_enabled`; переиспользуются `merge_lock_timeout_s`,
|
||||
`merge_gate_repos`. `false` → строго прежнее поведение.
|
||||
|
||||
## Альтернативы
|
||||
- Reaper внутри reconciler — отвергнуто (смешение stage- и jobs-уровней, общий
|
||||
kill-switch, хуже изоляция).
|
||||
- Только эвристика `agent_runs` без `jobs.pid` — отвергнуто как основной механизм
|
||||
(не ловит зомби, чей monitor умер до записи exit_code); оставлена как Tier-2/3.
|
||||
- БД-lock / внешний брокер очередей — вне объёма (single-node SQLite).
|
||||
- Форс `done` по факту exit0 — отвергнуто; выбран gate-driven advance.
|
||||
|
||||
## Последствия
|
||||
- (+) Зомби-job и залипший lease самовосстанавливаются без рестарта и без
|
||||
оператора; очередь общего инстанса не встаёт; снят технический блокер ORCH-54.
|
||||
- (+) Контракты неизменны (`STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, БАГ-8,
|
||||
exit-коды хука); одна колонка через проверенный idempotent-паттерн.
|
||||
- (−) pid-liveness валиден в предположении одного pid-namespace (агент —
|
||||
дочерний процесс оркестратора); закрыто backstop'ом по времени и TTL.
|
||||
- (−) streak-счётчик in-memory (сброс на рестарте; рестарт покрыт
|
||||
`requeue_running_jobs`).
|
||||
|
||||
## Связи
|
||||
- Базируется: adr-0002 (очередь), adr-0006 (merge-gate), adr-0007 (reconciler /
|
||||
self-deploy).
|
||||
- Разблокирует: ORCH-54.
|
||||
@@ -326,6 +326,7 @@ webhook (plane/gitea) background thread (queue_worker)
|
||||
| `status` | `queued` → `running` → `done` \| `failed` |
|
||||
| `attempts` / `max_attempts` | счётчик попыток (инкремент при claim) / лимит ретраев (default 2) |
|
||||
| `run_id` | FK на `agent_runs.id` после старта |
|
||||
| `pid` | (ORCH-065) pid агентского процесса (`proc.pid` из `_spawn`); liveness-сигнал для job-reaper. Добавляется `_ensure_column` (idempotent) |
|
||||
| `task_content` | ТЗ, которое пишется в task-файл агента |
|
||||
| `error` | последняя ошибка |
|
||||
|
||||
@@ -343,6 +344,36 @@ status='queued'` и проверяет `rowcount`. При гонке двух т
|
||||
jobs со статусом `running` (воркер умёр на рестарте) → возвращаются в `queued`.
|
||||
Потом стартует воркер; на shutdown — `worker.stop()` (Event.set + join).
|
||||
|
||||
### Job-reaper (ORCH-065, рестарт НЕ требуется)
|
||||
|
||||
`requeue_running_jobs()` спасает ТОЛЬКО на старте процесса. Зомби-job, возникший
|
||||
**без** рестарта (умер monitor-поток/дочерний процесс, а сервис жив), оставался
|
||||
`running` навсегда и при `max_concurrency=1` блокировал всю очередь. Фоновый
|
||||
daemon-поток `src/job_reaper.py` (каркас `reconciler`) периодически
|
||||
(`reaper_interval_s`) сканирует `running`-jobs и реапит «мёртвые»:
|
||||
- **Tier-1** — `jobs.pid` мёртв (`os.kill(pid,0)`→`ProcessLookupError`) на
|
||||
протяжении `reaper_dead_ticks` подряд тиков (анти-ложноположительность);
|
||||
- **Tier-2** — у `agent_runs[run_id]` записан `exit_code`, а `jobs.status` ещё
|
||||
`running`. Окно неоднозначно: живой monitor пишет `exit_code` ПЕРВЫМ, затем
|
||||
git push/PR/Plane-комментарии (секунды-десятки секунд) и лишь потом
|
||||
`_finalize_job`; pid агента к этому моменту мёртв в обоих случаях. Поэтому
|
||||
Tier-2 реапит только после finalization-grace `reaper_finalize_grace_s`
|
||||
(`finished_age_s >= grace`) — живой финализирующий monitor НЕ реапится;
|
||||
- **Tier-3** — backstop: job висит `running` дольше `reaper_max_running_s`.
|
||||
|
||||
Реап атомарен (`UPDATE jobs SET ... WHERE id=? AND status='running'` + `rowcount`,
|
||||
как `claim_next_job`) → совместим со стартовым `requeue_running_jobs` без двойной
|
||||
обработки. Действие — **claim-before-act**: для exit0 канонический QG оценивается
|
||||
read-only ПЕРЕД атомарным claim, затем claim `done` ПЕРВЫМ и только победитель
|
||||
claim делает `_try_advance_stage` (advance+enqueue) — проигравший (поздний monitor
|
||||
/ стартовый requeue) не выполняет побочных эффектов (нет дубль-advance/-enqueue);
|
||||
источник истины — QG, не «exit0»; гейт красный или exit≠0/неизвестно →
|
||||
`attempts<max`→`queued`, иначе `failed`+Telegram. Тот же поток на старте и
|
||||
периодически делает проактивный реклейм stale/dead merge-lease (`merge_gate.py`:
|
||||
`pid_alive`/`reclaim_stale_lease`). never-raise; kill-switch `ORCH_REAPER_ENABLED`
|
||||
/ `ORCH_LEASE_RECLAIM_ENABLED`; снимок в `GET /queue` (блок `reaper`). Подробнее —
|
||||
adr-0011.
|
||||
|
||||
### Конфиг
|
||||
|
||||
- `ORCH_MAX_CONCURRENCY` (default 1) — лимит параллельных jobs.
|
||||
|
||||
78
docs/history/LESSONS_2026-06-07_autonomy-closure.md
Normal file
78
docs/history/LESSONS_2026-06-07_autonomy-closure.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Lessons Learned — 2026-06-07: замыкание автономности self-deploy (5 задач в прод)
|
||||
|
||||
## Итог
|
||||
За одну сессию закрыты в прод **5 задач**, завершающих автономный self-deploy эпика ORCH-54:
|
||||
|
||||
| Задача | Что | Прод-коммит |
|
||||
|--------|-----|-------------|
|
||||
| ORCH-58 | provenance retag-guard (свежесть staging-образа перед BUILD-ONCE) | 094b5e2 |
|
||||
| ORCH-60 | reconciler не трогает escalated/Blocked/Needs-Input | d4c6cc0 |
|
||||
| ORCH-61 | фикс петли deploy-staging (staging_verdict: waive sandbox-infra FAILs C9a/C9b) | e18947d |
|
||||
| ORCH-21 | post-deploy мониторинг прода + auto-rollback (self-hosting=alert-only) | f85e449 |
|
||||
| ORCH-65 | job-reaper + stale merge-lease reclaim + idempotent merge | bb03350 |
|
||||
|
||||
**Главное:** после ORCH-60/61 конвейер впервые провёз задачи (ORCH-21/65) через deploy-staging
|
||||
**автономно** без отката; после ORCH-65 (job-reaper в проде) зомби-job и зависшие merge-lease
|
||||
лечатся сами. Последняя ручная точка автономного деплоя закрыта.
|
||||
|
||||
---
|
||||
|
||||
## Класс багов: «процесс умер — ресурс захвачен навсегда» (ORCH-65)
|
||||
Три связанных отказа, все воспроизвелись на ORCH-58/60/61/21:
|
||||
- **zombie jobs:** агент завершился/умер, строка jobs осталась running. requeue_running_jobs()
|
||||
спасает только на старте процесса; зомби без рестарта не лечился → при concurrency=1 встаёт
|
||||
конвейер ВСЕХ проектов. (jobs 236/239/242/254/265 — все зомби за сессию.)
|
||||
- **stale merge-lease:** merge-gate берёт .merge-lease-<repo>.json, делает rebase+re-test green,
|
||||
а на финальном merge процесс умирает с зажатым lease → merge не докатывается.
|
||||
- **неидемпотентный merge:** re-drive повторно пытается слить уже слитый PR.
|
||||
Фикс: фоновый job_reaper (паттерн reconciler, dead_ticks streak + мёртвый pid + exit_code,
|
||||
атомарный reap-claim, never-raise, kill-switch, снимок в /queue) + проактивный lease-reclaim
|
||||
по pid + guard pr_already_merged ПЕРЕД merge.
|
||||
|
||||
## Петля deploy-staging (ORCH-61) — ДВЕ причины
|
||||
1. ложный check_staging_status FAILED: staging_check падает на C9a/C9b (sandbox e2e branch +
|
||||
analyst-job-in-queue), т.к. bot-токены SANDBOX-проекта не настроены — НЕ регресс кода.
|
||||
2. no-changes для action-стадий (деплой = рестарт/retag, не правка → коммитить нечего).
|
||||
Фикс: staging_verdict waive sandbox-infra-only FAILs.
|
||||
|
||||
## Инфра-каскад от переполненного диска (инцидент дня)
|
||||
- Частые build-once/--build-staging пересборки за день забили docker build cache до 11 ГБ →
|
||||
диск 100% → CI red (No space left).
|
||||
- ДАЖЕ после чистки диска Gitea осталась в сломанном состоянии: внутренняя queue
|
||||
(/data/gitea/queues/common/*.log) залипла → post-receive hook 500 → actions tasks НЕ
|
||||
создаются, CI не триггерится вовсе (статус пустой, не failure). runner при этом online+idle.
|
||||
- Лечение: docker builder prune -af + рестарт Gitea (queue распускается → CI ожил).
|
||||
|
||||
---
|
||||
|
||||
## Уроки
|
||||
1. **Self-hosting safety (сквозной принцип):** прод-орк обслуживает ВСЕ проекты. Нельзя авто-
|
||||
откатывать/рестартить self в рамках задачи; нельзя пушить main. ORCH-21 post-deploy для
|
||||
self-hosting = alert-only, авто-rollback только для не-self репо.
|
||||
2. **TDD без доводки (повтор ORCH-58 и ORCH-65 v1):** тесты есть, реализация/wiring не
|
||||
подключены к боевому пути → мёртвый код + врущая дока. Reviewer обязан грепать вызовы из
|
||||
прод-кода, не только наличие функции.
|
||||
3. **Concurrency-баги ловятся итеративно:** ORCH-65 3 прохода reviewer (мёртвый guard → race
|
||||
condition side-effects-before-claim → approve) — каждый раз НОВЫЙ реальный дефект, не
|
||||
зацикливание. Atomic-claim ДО side-effects — обязательное правило.
|
||||
4. **При красном CI + зелёных локальных тестах — ПЕРВЫМ делом df -h / и docker system df**,
|
||||
не копаться в коде. После disk-full обязателен рестарт Gitea (queue залипает).
|
||||
5. **Bootstrap-разрыв:** задача про автономность деплоя не может задеплоить себя автономно,
|
||||
пока её механизм не в проде. Последний прод-деплой каждого такого фикса — вручную.
|
||||
6. **Перед прод-retag (build-once SOURCE_IMAGE=staging):** проверить revision-label staging-
|
||||
образа == целевой main HEAD, иначе guard fail-closed (by design). Если != → пересобрать
|
||||
--build-staging GIT_SHA=<main HEAD>.
|
||||
|
||||
## Ручная доводка прод-deploy (схема до ORCH-65 в проде)
|
||||
cancel zombie job → park task In Progress → merge PR (Gitea pulls/{n}/merge Do=merge, CI green)
|
||||
→ --build-staging GIT_SHA=<main HEAD> (проставит label) → rollback-снимок → --deploy с
|
||||
EXPECTED_REVISION=<sha> (guard сверит → retag → health 200) → Plane Done + UPDATE tasks stage=done.
|
||||
|
||||
## Follow-up (Backlog)
|
||||
- ORCH-62: авто-prune docker build cache (cron/daemon.json defaultKeepStorage).
|
||||
- ORCH-63: мониторинг диска mva154 + алерт >85%.
|
||||
- ORCH-64: починить NTP/часы mva154 (ушли ~+3ч от UTC).
|
||||
|
||||
## Осталось в эпике ORCH-54
|
||||
ORCH-22 (security-гейт), ORCH-59 (Confirm Deploy статус), ORCH-23 (budget circuit-breaker),
|
||||
P2: ORCH-57, ORCH-51.
|
||||
120
docs/history/LESSONS_ORCH-036-053.md
Normal file
120
docs/history/LESSONS_ORCH-036-053.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Lessons Learned — 2026-06-06 (вечер): ORCH-36 + ORCH-53 → прод (эпик ORCH-54)
|
||||
|
||||
## Итог
|
||||
Закрыты две задачи эпика ORCH-54 (автономное внедрение): **ORCH-36** (исполняемый
|
||||
самодеплой стадии `deploy`) и **ORCH-53** (sweeper/reconciler потерянных webhook).
|
||||
Обе прошли конвейер через рабочий merge-gate (ORCH-43), но финальный мерж+деплой
|
||||
потребовал **ручного разрыва bootstrap-цикла** — задача, добавляющая автодеплой, сама
|
||||
не может задеплоить себя через старую логику. Reconciler доказал себя **в первую секунду
|
||||
после старта** — разблокировал две реально застрявшие задачи (ORCH-036 и ET-013).
|
||||
|
||||
Эпик ORCH-54: **4 из 6 в проде** (ORCH-40 права, ORCH-43 merge-gate, ORCH-36 деплой,
|
||||
ORCH-53 reconciler). Осталось: ORCH-51 (окно/HA), обкатка полностью автономного деплоя.
|
||||
|
||||
---
|
||||
|
||||
## 1. 🔴 Bootstrap-парадокс самодеплоя (ORCH-36)
|
||||
|
||||
### Симптом
|
||||
ORCH-36 застряла в петле `deploy → development`:
|
||||
```
|
||||
QG check_deploy_status — failed: Deploy log not found (14-deploy-log.md)
|
||||
→ deployer verdict FAILED, rolled back deploy → development
|
||||
```
|
||||
deployer запускался (exit 0), но **не писал** `14-deploy-log.md` → гейт FAILED → откат →
|
||||
снова deployer → бесконечный цикл (jobs 140→142→143...).
|
||||
|
||||
### Корень
|
||||
Классический bootstrap самохостинга: **новая deploy-логика лежит в ветке, старая работает
|
||||
в проде**. ORCH-36 учит deployer писать лог по результату РЕАЛЬНОГО деплоя (через хост-хук),
|
||||
но прод-deployer работает по СТАРОМУ промпту, который для self-репо реального деплоя не делает
|
||||
и SUCCESS-лог не пишет. Нет лога → FAILED → откат.
|
||||
|
||||
### Урок
|
||||
**Self-репо не может задеплоить сам себя через старую логику.** Нужен разовый ручной разрыв
|
||||
цикла: домержить + задеплоить руками ОДИН раз, дальше конвейер катит своей же новой логикой.
|
||||
Тот же паттерн был у ORCH-40/43. Это структурное свойство любой задачи, меняющей
|
||||
deploy/merge-механику самого оркестратора — закладывать ручной bootstrap-шаг в план.
|
||||
|
||||
---
|
||||
|
||||
## 2. 🔴 Merge-конфликт при последовательном ручном мерже двух задач
|
||||
|
||||
### Симптом
|
||||
PR #56 (ORCH-53) смержен первым — чисто. PR #55 (ORCH-36) сразу после → **CONFLICT 409**:
|
||||
`.env.example`, `CHANGELOG.md`, `docs/architecture/README.md`, `docs/operations/INFRA.md`,
|
||||
**`src/config.py`**.
|
||||
|
||||
### Корень
|
||||
После мержа PR #56 `main` ушёл вперёд → PR #55 валидировался против СТАРОГО main (точки
|
||||
ответвления), а мержится в НОВЫЙ. Это ровно класс «main ушёл вперёд», который чинит
|
||||
merge-gate (ORCH-43) — но при РУЧНОМ мерже через Gitea API merge-gate не участвует.
|
||||
|
||||
### Решение
|
||||
- **merge main→ветку, НЕ rebase.** Rebase 9 коммитов = 9 потенциальных конфликт-разборов;
|
||||
один merge-коммит = ОДИН разбор. Быстрее и безопаснее для большого набора коммитов.
|
||||
- Конфликт в `src/config.py` был чисто **аддитивный**: ветка ORCH-36 добавляла блок
|
||||
`self_deploy_*` настроек, main (ORCH-53) — блок `reconcile_*`. Нужны **ОБА** блока →
|
||||
склеить, убрав только git-маркеры (`<<<<<<<`/`=======`/`>>>>>>>`). Обязательно после —
|
||||
`python3 -c 'import ast; ast.parse(...)'` для проверки синтаксиса.
|
||||
- docs/.env/CHANGELOG конфликты — тоже аддитивные (обе стороны добавляют строки) → union.
|
||||
|
||||
### Грабли
|
||||
⚠️ `grep -rE '^(<<<<<<<|=======|>>>>>>>)'` по `docs/work-items/*/13-test-report.md` даёт
|
||||
**ЛОЖНЫЕ срабатывания** — там `=======` это markdown-разделители таблиц/секций, не
|
||||
git-конфликты. Проверять реальные конфликтные файлы поимённо, не доверять глобальному grep.
|
||||
|
||||
---
|
||||
|
||||
## 3. Review-гейт поймал 2 реальных P1 ДО прода (ORCH-36)
|
||||
|
||||
reviewer завернул первую версию (`verdict: REQUEST_CHANGES`), конвейер сам откатил
|
||||
dev→review→fix→APPROVED. Два P1:
|
||||
1. **sentinel-маркеры self-deploy (`initiated`/`result`/`approve-requested`) не чистились на
|
||||
rollback** → при возврате задачи человек ставит Approved, а устаревший маркер ломает фазу B.
|
||||
2. **нет `.env.example` для новых флагов** + процедуры «approve→деплой» в `INFRA.md`.
|
||||
|
||||
Урок: merge-gate + review отрабатывают как задумано — брак не уходит в прод автономно.
|
||||
Это и есть ценность эпика: система фильтрует сама.
|
||||
|
||||
---
|
||||
|
||||
## 4. 🔥 Reconciler доказал себя мгновенно (ORCH-53)
|
||||
|
||||
В первую секунду после рестарта прода (21:24 UTC):
|
||||
```
|
||||
reconciler: ORCH-036 development разблокирована (потерян webhook)
|
||||
reconciler: ET-013 development разблокирована (потерян webhook)
|
||||
```
|
||||
Sweeper нашёл и разблокировал ДВЕ реально застрявшие задачи — включая саму ORCH-036 из
|
||||
bootstrap-петли, и старое зависание ET-013 (enduro-trails). Ручной heartbeat-watchdog,
|
||||
который раньше держал Стрим, **больше не нужен** — система чинит застревания сама.
|
||||
|
||||
---
|
||||
|
||||
## 5. Операционные мелочи (закрепить)
|
||||
|
||||
- **Заголовки ORCH-задач ≤80 символов.** QG-0 (`check title length`) заворачивает старт
|
||||
конвейера, если длиннее. ORCH-53 был 83 символа → завернул на старте → подрезали до 71.
|
||||
- **Developer-таймаут 1800с (30 мин) мал для мясных задач.** 1-й заход developer'а ORCH-36
|
||||
(деплой-хук + Telegram-кнопка + callback) упёрся в лимит → SIGKILL (exit -9). Спас
|
||||
resilience-ретрай (ORCH-1b): attempt 2, наработки в worktree между попытками сохранились.
|
||||
Если упирается систематически — поднять `agent_timeout_seconds` (override per-agent) или
|
||||
дробить задачу.
|
||||
- **Время хоста ≠ UTC.** Файлы worktree датируются по мск (+3), БД/системное — UTC. Не баг,
|
||||
но путает сверки `etime`/`updated_at`/`finished_at`. Сверять по одному источнику.
|
||||
- **Gitea merge auth:** заголовок строго `Authorization: token <ORCH_GITEA_TOKEN>` (формат
|
||||
`token `, буквально). НЕ маскировать токен плейсхолдером `***` → иначе 401.
|
||||
POST `/repos/admin/orchestrator/pulls/{N}/merge`, body `{"Do":"merge"}`.
|
||||
- **approve прод-деплоя 8500 = Telegram-кнопка** (решение Owner), флаг
|
||||
`DEPLOY_REQUIRE_MANUAL_APPROVE=true` по дефолту.
|
||||
- **max_concurrency=1 оставлен сознательно** (решение Owner): одна БД/очередь на все
|
||||
проекты, последовательное выполнение надёжнее. НЕ поднимать без явного запроса.
|
||||
|
||||
---
|
||||
|
||||
## Состояние прода после деплоя (21:24 UTC, main `1ff8d85`)
|
||||
- `src/self_deploy.py` — в проде (исполняемый деплой, 3 фазы A/B/C)
|
||||
- `src/reconciler.py` — в проде (фоновый sweeper, уже разблокировал 2 задачи)
|
||||
- uid 1000, health `{"status":"ok"}`, preflight True (Claude Code 2.1.142)
|
||||
- Деплой-скрипт с авто-rollback: исходник в workspace `temp/deploy_36_53.sh`
|
||||
78
docs/history/LESSONS_ORCH-036-selfdeploy.md
Normal file
78
docs/history/LESSONS_ORCH-036-selfdeploy.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Lessons Learned — 2026-06-07 (утро): ORCH-36 self-deploy bootstrap — каскад неготовности инфры
|
||||
|
||||
## Итог
|
||||
ORCH-36 (исполняемый самодеплой стадии `deploy`) **замкнулась в проде** — конвейер
|
||||
впервые задеплоил сам себя по полному циклу Phase A→B→C (approve → детачед ssh-хук →
|
||||
finalizer). Но путь до Done вскрыл **четыре слоя неготовности инфраструктуры**, каждый из
|
||||
которых требовал ручного bootstrap-разрыва: задача про автодеплой не может задеплоить
|
||||
сама себя, пока её же механизм + инфра не в проде.
|
||||
|
||||
Эпик ORCH-54: **4/6 в проде** (ORCH-40 права, ORCH-43 merge-gate, ORCH-36 самодеплой,
|
||||
ORCH-53 reconciler). Конвейер автономен: мержит → катит в прод → чинит застрявшее.
|
||||
|
||||
---
|
||||
|
||||
## Каскад из 4 инфра-багов (все вскрылись только при РЕАЛЬНОМ деплое)
|
||||
|
||||
### 1. 🔴 uid 1000 без записи в `/etc/passwd` → ssh/whoami падают
|
||||
**Симптом:** `self-deploy initiate failed: ssh launch failed (rc=255): No user exists for
|
||||
uid 1000`. **Корень:** регрессия ORCH-40 — compose запускает контейнер под `1000:1000`,
|
||||
но базовый образ `python:3.12-slim` не имеет passwd-записи для 1000. SSH-клиент (и
|
||||
`whoami`, `getpwuid()`) отказываются стартовать без валидного юзера.
|
||||
**Фикс:** в `Dockerfile` — `groupadd -g 1000 app && useradd -u 1000 -g 1000 -m -d
|
||||
/home/slin -s /bin/bash slin`. Rebuild + recreate. Коммит `64e031a`.
|
||||
**Урок:** при переводе контейнера на non-root uid (ORCH-40) ОБЯЗАТЕЛЬНО создавать passwd-
|
||||
запись в образе, иначе ssh/git/любой инструмент с getpwuid() ломается. Проверять
|
||||
`docker exec <c> whoami` после смены uid.
|
||||
|
||||
### 2. 🔴 env-префикс: `DEPLOY_*` vs `ORCH_DEPLOY_*` (pydantic не видит)
|
||||
**Симптом:** `ssh: Could not resolve hostname : No address associated with hostname` —
|
||||
host пустой, хотя в compose `DEPLOY_SSH_HOST=127.0.0.1` задан. **Корень:** `Settings`
|
||||
имеет `env_prefix = "ORCH_"` → читает ТОЛЬКО `ORCH_DEPLOY_SSH_HOST`. Старые
|
||||
`DEPLOY_*` (без префикса) предназначались легаси enduro-деплоеру (читает через
|
||||
`os.environ` напрямую) и pydantic их игнорирует → дефолт `host=""`. Доп: `DEPLOY_HOOK_SCRIPT`
|
||||
указывал на `enduro-deploy-hook.sh`, не на orchestrator-хук.
|
||||
**Фикс:** в `docker-compose.yml` добавлены `ORCH_DEPLOY_SSH_USER/HOST`,
|
||||
`ORCH_DEPLOY_HOOK_SCRIPT=scripts/orchestrator-deploy-hook.sh`,
|
||||
`ORCH_DEPLOY_HOST_REPO_PATH` (легаси `DEPLOY_*` оставлены для enduro). Коммит `115519e`.
|
||||
**Урок:** все настройки, читаемые через pydantic Settings, ДОЛЖНЫ иметь префикс `ORCH_`.
|
||||
Проверять резолв: `docker exec <c> python3 -c 'from src.config import settings; print(settings.deploy_ssh_host)'`.
|
||||
|
||||
### 3. 🔴 `/var/log/orchestrator` принадлежит root → хук падает на tee
|
||||
**Симптом:** `tee: /var/log/orchestrator/deploy-hook.log: Permission denied`, хук exit 1.
|
||||
**Корень:** лог-директория `root:root`, а хук бежит под `slin`. **Фикс:** `chown -R
|
||||
slin:slin /var/log/orchestrator` на хосте.
|
||||
**Урок:** все пути, в которые пишет хост-хук (логи, sentinel, prev-image), должны быть
|
||||
writable юзером, под которым ssh-сессия. Заложить создание/chown в provisioning хоста.
|
||||
|
||||
### 4. 🔴🔴 BUILD-ONCE retag берёт УСТАРЕВШИЙ staging-образ → катит регресс (ВАЖНО)
|
||||
**Симптом:** деплой «зелёный» (result=0, health ok), но прод откатился на код 2-дневной
|
||||
давности — пропал `deploy-finalizer` (`Unknown agent: deploy-finalizer`), задача не
|
||||
закрылась. **Корень:** хук делает `BUILD-ONCE: retag orchestrator-orchestrator-staging →
|
||||
orchestrator-orchestrator` (без rebuild, by design ORCH-36 BR-6). Дизайн предполагал
|
||||
«staging-образ = свежий, провалидированный». В РЕАЛЬНОСТИ `orchestrator-orchestrator-staging`
|
||||
никто не пересобрал из нового main → retag катил в прод СТАРЫЙ образ → бесконечная петля:
|
||||
каждый Phase B возвращал прод в прошлое, finalizer (новый код) исчезал, Phase C не мог
|
||||
закрыть задачу.
|
||||
**Фикс (ручной разрыв):** пересобрать `orchestrator-orchestrator-staging` из актуального
|
||||
main ПЕРЕД retag → тогда хук катит свежий код. После этого Phase C отработал: result=0 →
|
||||
SUCCESS → `deploy → done`.
|
||||
**Урок / ТЕХДОЛГ:** retag-стратегия BUILD-ONCE предполагает гарантию свежести staging-
|
||||
образа, которой НЕТ. Нужна отдельная задача: либо staging-деплой пересобирает образ из
|
||||
текущего main перед валидацией, либо deploy-хук проверяет, что staging-образ собран из
|
||||
HEAD main (по labels/sha), иначе fail-fast. Сейчас «зелёный» деплой может молча катить
|
||||
регресс. **Это самый опасный из четырёх — он не падает, а тихо откатывает прод.**
|
||||
|
||||
---
|
||||
|
||||
## Сквозной урок: bootstrap самохостинга
|
||||
Любая задача, меняющая deploy/merge-механику САМОГО оркестратора, упирается в парадокс:
|
||||
её механизм не работает, пока не в проде, а в прод его можно влить только старым
|
||||
механизмом. Каждый слой (код → права → env → образ) вскрывается ТОЛЬКО при первом
|
||||
реальном прогоне. Закладывать в план таких задач **ручной bootstrap-чеклист** и гонять
|
||||
**реальный** деплой в staging-петле до мержа, а не только бумажные гейты.
|
||||
|
||||
## Прод после (main `115519e`+, образ 2026-06-07 09:47)
|
||||
- self_deploy.py + reconciler.py в проде, finalizer зарегистрирован (grep=5)
|
||||
- uid 1000 = slin (passwd ok), ssh slin@127.0.0.1 работает, /var/log/orchestrator writable
|
||||
- ORCH-36 task 43 → done, Plane → Done
|
||||
@@ -9,6 +9,7 @@
|
||||
1. **Захват текущего образа** — до рестарта записывает ID образа работающего контейнера в `$PREV_IMAGE_FILE` (best-effort, не падает если сервис не запущен).
|
||||
2. **git pull** — обновляет код репозитория.
|
||||
2b. **Build-once retag** (ORCH-036, BR-6) — если задан `$SOURCE_IMAGE`, хук ретегает его на `$TARGET_IMAGE` (`docker tag $SOURCE_IMAGE $TARGET_IMAGE`) и поднимает контейнер на этом образе через `up -d --no-build`. Это деплой РОВНО того образа, что прошёл staging, **без `docker build`**. Если `$SOURCE_IMAGE` не задан (дефолт) — шаг пропускается (обратная совместимость).
|
||||
- **Fail-closed провенанс-guard** (ORCH-058, Strategy B) — ПЕРЕД `docker tag`, если задан `$EXPECTED_REVISION`, хук сверяет OCI-лейбл `org.opencontainers.image.revision` у `$SOURCE_IMAGE` с `$EXPECTED_REVISION`. Несовпадение / пустой лейбл (`<no value>`) / ошибка inspect → лог + `exit 1` (FAILED → авто-rollback), **прод не трогается**. Не задан `$EXPECTED_REVISION` (дефолт) → проверка пропускается (обратная совместимость для не-self репозиториев).
|
||||
3. **Рестарт контейнера** — `docker compose --profile $COMPOSE_PROFILE up -d --no-build $TARGET_SERVICE`.
|
||||
4. **Health-цикл** — 10 попыток × 6с = до 60с. Критерий: HTTP 200 + тело содержит `"status":"ok"`.
|
||||
- **Успех** → `exit 0`, лог "Deploy SUCCESS".
|
||||
@@ -17,6 +18,17 @@
|
||||
- Если восстановился → `exit 1` (деплой провалился, откат успешен).
|
||||
- Если и откат не помог → `exit 2` (критично).
|
||||
|
||||
### Режим `--build-staging` (ORCH-058, Strategy A)
|
||||
|
||||
Пересобирает **staging-образ** из провалидированного коммита и пересоздаёт 8501, чтобы артефакт, который мы валидируем, был РОВНО тем, что позже build-once ретегается в прод (инвариант `INV-FRESH`). Собирает/пересоздаёт **только staging (8501)** — никогда прод (8500).
|
||||
|
||||
1. `docker build --build-arg GIT_SHA=$GIT_SHA -t $TARGET_IMAGE $BUILD_CONTEXT` — пересборка из host-worktree валидированного коммита; `GIT_SHA` штампуется в OCI-лейбл `org.opencontainers.image.revision`.
|
||||
2. `docker compose [--profile $COMPOSE_PROFILE] up -d --no-build $TARGET_SERVICE` — пересоздание staging на свежем образе.
|
||||
3. Health-цикл 10×6с. Провал сборки/health → `exit 1`.
|
||||
4. **`staging_check` против СВЕЖЕГО образа** (Strategy A, шаг 3 — ADR-001, AC-4) — после health хук запускает `docker exec $STAGING_CONTAINER python3 $STAGING_CHECK_PATH --base-url http://localhost:$TARGET_PORT --mode $STAGING_CHECK_MODE` (дефолт `--mode stub`, без LLM-трат). Запуск **внутри** staging-контейнера канонический (ORCH-048): suite читает реестр из собственного env контейнера, а `staging_check.py` берётся из bind-mount (`/repos/orchestrator/scripts/...`, не из образа). Это ровно тот артефакт, что позже build-once ретегается в прод → валидируем то, что промоутим (AC-4). PASS → `exit 0`; любой не-ноль (FAIL чека или safety-abort `ORCH_STAGING≠true`) → `exit 1`.
|
||||
|
||||
Запускается оркестратором на ребре `deploy-staging → deploy` (QG-под-чек `check_staging_image_fresh` → `rebuild_staging_image` пробрасывает явный staging-таргет, см. `INFRA.md`). Тот же контракт кодов выхода (0 = здоров **и** staging_check PASS).
|
||||
|
||||
### Режим `--rollback`
|
||||
|
||||
Вручную откатывает сервис на предыдущий образ из `$PREV_IMAGE_FILE`.
|
||||
@@ -31,6 +43,12 @@
|
||||
| `COMPOSE_PROFILE`| `staging` | Docker compose profile (пусто = без профиля) |
|
||||
| `PREV_IMAGE_FILE`| `$REPO/.deploy-prev-image-staging`| Файл для сохранения предыдущего образа |
|
||||
| `SOURCE_IMAGE` | _(unset)_ | Build-once (ORCH-036): провалидированный образ для retag на `$TARGET_IMAGE` перед рестартом (без rebuild). Не задан → шаг пропущен. |
|
||||
| `EXPECTED_REVISION` | _(unset)_ | Build-once (ORCH-058, Strategy B): ожидаемый git-SHA `$SOURCE_IMAGE` (лейбл `org.opencontainers.image.revision`). Задан → fail-closed guard перед `docker tag`. Не задан → проверка пропущена. |
|
||||
| `GIT_SHA` | _(unset)_ | `--build-staging` (ORCH-058, Strategy A): коммит, штампуемый в OCI-лейбл `revision` при пересборке staging-образа. |
|
||||
| `BUILD_CONTEXT` | `$REPO` | `--build-staging`: docker build context (host-worktree валидированного коммита). |
|
||||
| `STAGING_CONTAINER` | `$TARGET_SERVICE` (`orchestrator-staging`) | `--build-staging` (ORCH-058): контейнер, внутри которого `docker exec` запускает `staging_check`. |
|
||||
| `STAGING_CHECK_PATH` | `/repos/orchestrator/scripts/staging_check.py` | `--build-staging` (ORCH-058): путь к `staging_check.py` внутри контейнера (bind-mount, не образ). |
|
||||
| `STAGING_CHECK_MODE` | `stub` | `--build-staging` (ORCH-058): режим `staging_check` (`stub` — быстро, без LLM; `full-real` — дожидается аналитика). |
|
||||
| `LOG` | `/var/log/orchestrator/deploy-hook.log` | Лог-файл (fallback: `$REPO/deploy-hook.log`) |
|
||||
|
||||
> ⚠️ **Дефолт — всегда STAGING**. Прод активируется только явным переопределением env.
|
||||
|
||||
@@ -83,6 +83,8 @@ ADR `docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md` и гл
|
||||
| `ORCH_DEPLOY_HOOK_SCRIPT` / `_HOST_REPO_PATH` | путь к хук-скрипту (отн. репо) и чекаут orchestrator на хосте |
|
||||
| `ORCH_DEPLOY_PROD_SOURCE_IMAGE` | staging-образ для build-once retag на прод-тег (без rebuild) |
|
||||
| `ORCH_DEPLOY_PROD_TARGET_SERVICE` / `_TARGET_PORT` / `_TARGET_IMAGE` / `_COMPOSE_PROFILE` / `_PREV_IMAGE_FILE` | прод-цель хука + снапшот для авто-rollback |
|
||||
| `ORCH_IMAGE_FRESHNESS_ENABLED` | ORCH-058 единый kill-switch провенанса staging-образа (A+B как целое); дефолт `true`, false → legacy build-once без проверки свежести |
|
||||
| `ORCH_IMAGE_FRESHNESS_REPOS` | CSV репозиториев с реальным гейтом свежести; пусто → только self-hosting `orchestrator` |
|
||||
| `ORCH_RECONCILE_ENABLED` | kill-switch sweeper потерянных webhook (ORCH-053); дефолт `true`. **При инциденте/раскатке** — `false` глушит весь фоновый reconciler |
|
||||
| `ORCH_RECONCILE_PLANE_ENABLED` | отдельный флаг F-2 (опрос Plane API); `false` гасит только plane-ветку, F-1 продолжает работать; дефолт `true` |
|
||||
| `ORCH_RECONCILE_INTERVAL_S` | период фонового прохода reconciler, сек; дефолт `120` |
|
||||
@@ -131,6 +133,7 @@ ADR `docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md` и гл
|
||||
|
||||
**Страховки:**
|
||||
- Стадия `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.
|
||||
|
||||
**Правила для агентов при задачах ORCH:**
|
||||
1. НЕ перезапускать / не ронять прод-контейнер `orchestrator` в рамках задачи.
|
||||
|
||||
@@ -75,6 +75,27 @@ completely invisible to commands that do not pass `--profile staging`.
|
||||
docker logs -f orchestrator-staging
|
||||
```
|
||||
|
||||
## Staging-образ как источник прод-артефакта (ORCH-058)
|
||||
|
||||
Прод-деплой орка — **build-once**: хук ретегает провалидированный staging-образ
|
||||
(`orchestrator-orchestrator-staging`) на прод-тег **без rebuild** (ORCH-036). Чтобы
|
||||
в прод не попал устаревший образ (инцидент LESSONS_ORCH-036 п.4), ORCH-058 гарантирует
|
||||
свежесть staging-образа **двумя слоями** (только self-hosting):
|
||||
|
||||
- **A — пересборка staging (liveness):** на ребре `deploy-staging → deploy` (ПОСЛЕ
|
||||
merge-gate, ДО Phase A) QG-под-чек `check_staging_image_fresh` через хук
|
||||
`--build-staging` пересобирает staging-образ из worktree валидированного коммита
|
||||
(`--build-arg GIT_SHA=<sha>`, OCI-лейбл `org.opencontainers.image.revision`) и
|
||||
пересоздаёт 8501. Так валидируем РОВНО тот артефакт, что промоутится в прод.
|
||||
FAIL → откат на `development`. Сборки/recreate — **только staging (8501)**.
|
||||
- **B — fail-closed guard (safety):** прод-хук перед `docker tag` сверяет лейбл
|
||||
`revision` у `SOURCE_IMAGE` с `EXPECTED_REVISION` (пробрасывает оркестратор);
|
||||
несовпадение / пустой лейбл / ошибка inspect → `exit 1`, прод не трогается.
|
||||
|
||||
Kill-switch `ORCH_IMAGE_FRESHNESS_ENABLED` включает A+B **как целое**; область —
|
||||
`ORCH_IMAGE_FRESHNESS_REPOS` (пусто → только `orchestrator`). Детали — `DEPLOY_HOOK.md`,
|
||||
`docs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md`.
|
||||
|
||||
## Roadmap
|
||||
|
||||
| Task | Description |
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
| B | ACCESS | Plane sandbox (R), Gitea sandbox (R+push), реестр проектов |
|
||||
| C | E2E | Создать задачу → триггер конвейера → ветка + коммент → cleanup |
|
||||
|
||||
Exit code: **0** = все PASS, **non-zero** = есть FAIL.
|
||||
Exit code: **0** = advance (все REAL-проверки PASS), **1** = rollback (есть REAL-FAIL).
|
||||
С ORCH-061 exit 0 может включать *waived* sandbox-infra FAIL (C9a/C9b) — см.
|
||||
[«Толерантность к sandbox-infra (ORCH-061)»](#толерантность-к-sandbox-infra-orch-061).
|
||||
|
||||
---
|
||||
|
||||
@@ -85,6 +87,56 @@ B6 «Registry: sandbox present, prod ET/ORCH absent» подтверждает
|
||||
|
||||
---
|
||||
|
||||
## Толерантность к sandbox-infra (ORCH-061)
|
||||
|
||||
**Проблема.** Self-hosting `orchestrator` зацикливался на `deploy-staging → development`:
|
||||
прежде скрипт давал exit 1 при **любом** FAIL, поэтому две чисто инфраструктурные
|
||||
проверки — **C9a** (ветка не появилась в `orchestrator-sandbox`) и **C9b** (job
|
||||
аналитика не встал в очередь staging) — приводили к `staging_status: FAILED` →
|
||||
откат → цикл. Корень: SANDBOX-бот-аккаунты не состоят в sandbox-проекте Plane,
|
||||
поэтому шаги 6+ конвейера в песочнице недостижимы. Это **не** регресс конвейера.
|
||||
|
||||
**Решение.** Проверки классифицируются на две категории (`src/staging_verdict.py`):
|
||||
|
||||
| Категория | Что входит | Поведение |
|
||||
|-----------|-----------|-----------|
|
||||
| `REAL` | все проверки конвейера (A*, B*, C7, C8) | **fail-closed** — любой FAIL = rollback |
|
||||
| `SANDBOX_INFRA` | строго allowlist `{C9a, C9b}` | **waivable** — FAIL терпится, если все REAL зелёные |
|
||||
|
||||
Вердикт сворачивается в `compute_staging_verdict(items, infra_tolerant)`:
|
||||
|
||||
- любой REAL-FAIL → `FAILED` / exit 1 (страховка сохраняется при ЛЮБОМ значении флага);
|
||||
- упали **только** C9a/C9b и толерантность включена → `SUCCESS` / exit 0,
|
||||
упавшие метки попадают в `waived` (наблюдаемость, печатается строкой `INFRA-WAIVED:`);
|
||||
- упали только C9a/C9b, толерантность выключена → `FAILED` / exit 1 (legacy-строгий);
|
||||
- любая внутренняя ошибка вердикта → `FAILED` / exit 1 (никогда не ложный green).
|
||||
|
||||
Blast-radius waiver-а ровно две allowlist-метки; всё неизвестное классифицируется
|
||||
как `REAL` (fail-closed).
|
||||
|
||||
### Kill-switch и `--strict`
|
||||
|
||||
| Управление | Эффект |
|
||||
|-----------|--------|
|
||||
| env `ORCH_STAGING_INFRA_TOLERANCE_ENABLED` (default `true`) | глобальный флаг; `false` → строгий режим (1:1 до ORCH-061) |
|
||||
| CLI `--strict` | форсит строгий режим для одного запуска, игнорируя env |
|
||||
|
||||
Флаг живёт в `.env.staging` (staging-инстанс). `--strict` имеет приоритет над env.
|
||||
|
||||
### Что печатает скрипт
|
||||
|
||||
В конце прогона `summary()` показывает разбивку REAL/SANDBOX_INFRA, затем:
|
||||
|
||||
```
|
||||
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox; C9b Analyst job enqueued ...
|
||||
VERDICT: SUCCESS (infra-waived): ['C9a …', 'C9b …'] are known sandbox-infra checks; all real checks green
|
||||
```
|
||||
|
||||
Контракт `staging_status: SUCCESS|FAILED` во frontmatter **не меняется** —
|
||||
толерантность применяется в скрипте ДО записи артефакта деплоером.
|
||||
|
||||
---
|
||||
|
||||
## Режимы (`--mode`)
|
||||
|
||||
| Режим | Описание | Скорость |
|
||||
|
||||
7
docs/work-items/ORCH-021/00-business-request.md
Normal file
7
docs/work-items/ORCH-021/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: [★ высокий] Post-deploy мониторинг прода + авто-rollback при деградации
|
||||
|
||||
Work Item ID: ORCH-021
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
88
docs/work-items/ORCH-021/01-brd.md
Normal file
88
docs/work-items/ORCH-021/01-brd.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# BRD — ORCH-021: Post-deploy мониторинг прода + авто-rollback при деградации
|
||||
|
||||
Work Item: ORCH-021
|
||||
Приоритет: высокий (★)
|
||||
Источник: предложение Стрим, одобрено Славой (2026-06-04)
|
||||
Стадия: analysis
|
||||
|
||||
## 1. Проблема (Why)
|
||||
|
||||
Сейчас конвейер заканчивается на `deploy → done`: как только `check_deploy_status`
|
||||
видит `deploy_status: SUCCESS`, задача закрывается и оркестратор **забывает про прод**.
|
||||
«Успех» деплоя сегодня означает только то, что health-check в момент рестарта
|
||||
прошёл (10×6с в `scripts/orchestrator-deploy-hook.sh`) — узкое окно ~60 секунд.
|
||||
|
||||
**Прямой урок ET-8:** деплой отрапортовал SUCCESS, а на проде фича не работала.
|
||||
Класс инцидентов — «зелёный деплой, красный прод»:
|
||||
- деградация проявляется через минуты, а не в первые 60с (прогрев кэшей, фоновые
|
||||
миграции, отложенные запросы, утечки, рост 5xx под реальным трафиком);
|
||||
- health-эндпоинт отвечает `200 ok`, но ключевая функциональность сломана;
|
||||
- регресс виден только под боевым трафиком, которого нет в момент рестарта.
|
||||
|
||||
После закрытия задачи никакого пригляда за продом нет — деградацию замечает человек
|
||||
постфактум. Для self-hosting это особенно опасно: сломанный прод-орк (8500) обслуживает
|
||||
ВСЕ проекты (enduro-trails) из общего инстанса.
|
||||
|
||||
## 2. Цель (What)
|
||||
|
||||
Продлить ответственность конвейера за прод **после** `deploy → done`: в течение
|
||||
заданного окна наблюдать ключевые сигналы здоровья прода и при доказанной деградации
|
||||
выполнить реакцию (откат на предыдущий образ или громкий алерт с запросом ручного
|
||||
отката). Закрыть класс «зелёный деплой, красный прод».
|
||||
|
||||
Механизм частичного отката уже есть: `do_rollback()` и режим `--rollback` в
|
||||
`scripts/orchestrator-deploy-hook.sh` умеют вернуть предыдущий образ из
|
||||
`PREV_IMAGE_FILE` (`.deploy-prev-image-prod`), который сохраняется при каждом деплое.
|
||||
Задача — построить **наблюдение поверх** этого и привязать решение к измеримым порогам.
|
||||
|
||||
## 3. Заинтересованные стороны
|
||||
- **Owner (Слава)** — принимает риск авто-отката прода; получает алерты.
|
||||
- **Стрим** — инициатор; потребитель сигнала деградации для петли уроков (ORCH-8).
|
||||
- **Другие проекты (enduro-trails)** — косвенно: устойчивость общего инстанса.
|
||||
|
||||
## 4. Бизнес-требования
|
||||
|
||||
| # | Требование | Приоритет |
|
||||
|---|------------|-----------|
|
||||
| BR-1 | После `deploy → done` прод наблюдается в течение конфигурируемого окна (дефолт ~15 мин), а не забывается. | Must |
|
||||
| BR-2 | Деградация определяется по **детерминированным измеримым сигналам**: периодический `/health` (HTTP 200 + `{"status":"ok"}`) и доля HTTP 5xx на ключевых эндпоинтах (`/status`, `/queue`). | Must |
|
||||
| BR-3 | Деградация фиксируется только по **порогам** (N последовательных провалов / окно), а не по разовому сетевому глюку — чтобы не было ложных откатов. | Must |
|
||||
| BR-4 | При подтверждённой деградации система выполняет реакцию: **авто-rollback** на `.deploy-prev-image-prod` (через существующий хук `--rollback`) **либо** громкий алерт с запросом ручного отката — в зависимости от политики репозитория. | Must |
|
||||
| BR-5 | **Self-hosting safety:** для самого `orchestrator` авто-откат прода = рестарт инструмента, обслуживающего все проекты. По умолчанию для self-hosting реакция — **алерт + ручной approve отката** (по образцу deploy Phase A/B), НЕ автоматический откат. Для не-self репозиториев допустим авто-откат. | Must |
|
||||
| BR-6 | Любой исход (наблюдение начато, деградация, откат, откат-провал, окно завершилось чисто) уведомляется в Telegram и комментарием в Plane; результат наблюдения фиксируется артефактом. | Must |
|
||||
| BR-7 | Мониторинг — **restart-safe**: рестарт оркестратора (в т.ч. сам деплой) не теряет и не задваивает наблюдение. Идемпотентность по образцу reconciler / deploy-finalizer. | Must |
|
||||
| BR-8 | Глобальный kill-switch (env-флаг) и список репозиториев, на которые распространяется фича (по образцу `merge_gate_enabled` / `image_freshness_enabled` / `self_deploy_repos`). Выключенный флаг = прежнее поведение (наблюдения нет). | Must |
|
||||
| BR-9 | Наблюдаемость: текущее состояние пост-деплой наблюдения отражается в `GET /queue` (по образцу блока `reconcile`). | Should |
|
||||
| BR-10 | Сигнал деградации пригоден для будущей петли уроков (ORCH-8): фиксируется в артефакте/логе в машиночитаемом виде. | Should |
|
||||
| BR-11 | Доменный smoke результата фичи (проверка, что конкретная фича реально работает) — желателен, но выносится в follow-up; MVP ограничивается health + 5xx. | Could |
|
||||
|
||||
## 5. Вне рамок (Out of scope)
|
||||
- Полноценная система метрик/APM (Prometheus, дашборды) — фича опирается на уже
|
||||
существующие HTTP-эндпоинты, не вводит сбор метрик.
|
||||
- Универсальный доменный smoke для произвольной фичи (BR-11 — follow-up).
|
||||
- Полностью автоматический откат прод-орка без участия человека (противоречит
|
||||
self-hosting safety; отдельная задача при наборе доверия, аналогично ORCH-54 для deploy).
|
||||
- Изменение момента вердикта `deploy_status` / контракта `check_deploy_status`
|
||||
(наблюдение происходит ПОСЛЕ `done`, не заменяет deploy-gate).
|
||||
|
||||
## 6. Связи
|
||||
- **ET-8** — прецедент «deploy SUCCESS, прод не работает». Обоснование задачи.
|
||||
- **ORCH-36** (`docs/architecture/adr/adr-0007-executable-self-deploy.md`) — Phase A/B/C
|
||||
исполняемого самодеплоя; пост-деплой наблюдение продлевает ответственность ЗА `done`,
|
||||
переиспользует sentinel-паттерн и detached-host-процесс для self-rollback.
|
||||
- **ORCH-53** (`src/reconciler.py`) — каноничный паттерн фонового daemon-потока
|
||||
(watchdog), запускаемого в `main.lifespan`; образец для пост-деплой наблюдателя.
|
||||
- **ORCH-58** — `.deploy-prev-image` и хук-механика отката, на которые опирается реакция.
|
||||
- **ORCH-8** — деградация прода = сигнал для петли уроков (BR-10).
|
||||
- **ORCH-12** — фича может оформиться как пост-deploy стадия ИЛИ как watchdog (решение
|
||||
архитектора, см. §7).
|
||||
|
||||
## 7. Открытые архитектурные вопросы (для архитектора, НЕ решаются в анализе)
|
||||
1. **Где живёт наблюдение:** отдельная пост-deploy стадия конвейера vs фоновый
|
||||
watchdog-daemon (по образцу `reconciler`) vs reserved-agent job (по образцу
|
||||
`deploy-finalizer`). Анализ задаёт требования (BR-1, BR-7), выбор механизма — за архитектором.
|
||||
2. **Механизм self-rollback для self-hosting:** откат прод-орка требует detached
|
||||
host-процесса (контейнер не может надёжно откатить себя, умирая) — переиспользовать
|
||||
ли `self_deploy.initiate_deploy` / хук `--rollback`.
|
||||
3. Точные пороги и веса сигналов (BR-3) — анализ предлагает дефолты (см. AC), архитектор
|
||||
фиксирует реализацию.
|
||||
165
docs/work-items/ORCH-021/02-trz.md
Normal file
165
docs/work-items/ORCH-021/02-trz.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# ТЗ — ORCH-021: Post-deploy мониторинг прода + авто-rollback
|
||||
|
||||
Work Item: ORCH-021
|
||||
Стадия: analysis → (architecture)
|
||||
|
||||
> Документ описывает ТРЕБОВАНИЯ к изменениям и НАЗЫВАЕТ задействованные модули.
|
||||
> Выбор механизма (стадия vs watchdog vs reserved-agent) и точная реализация —
|
||||
> зона архитектора (см. BRD §7). Здесь фиксируется, ЧТО должно измениться и КАКИЕ
|
||||
> контракты НЕЛЬЗЯ ломать.
|
||||
|
||||
## 1. Контекст в коде (как есть сейчас)
|
||||
|
||||
- Конвейер заканчивается в `src/stages.py`: `deploy → done`, gate `check_deploy_status`.
|
||||
Терминальный переход `deploy → done` исполняется в `src/stage_engine.py::advance_stage`
|
||||
(блок «Terminal sync», `set_issue_done`, release merge-lease). После этого ничего
|
||||
не наблюдает за продом.
|
||||
- `scripts/orchestrator-deploy-hook.sh` уже умеет:
|
||||
- `health_check(max_attempts, sleep, label)` — опрос `http://localhost:$TARGET_PORT/health`
|
||||
с проверкой `"status":"ok"`;
|
||||
- `do_rollback()` — retag `PREV_IMAGE_FILE` → `TARGET_IMAGE` + рестарт + пост-rollback
|
||||
health-check; коды возврата 0 (ок) / 1 (нет prev-образа) / 2 (rollback тоже упал);
|
||||
- режим `--rollback` (ручной откат);
|
||||
- при обычном деплое сохраняет `PREV_IMG` в `PREV_IMAGE_FILE`
|
||||
(`.deploy-prev-image-prod` для прода, см. `settings.deploy_prod_prev_image_file`).
|
||||
- Self-deploy прода идёт через detached host-процесс: `src/self_deploy.py`
|
||||
(`build_deploy_command`, `initiate_deploy`, sentinel-маркеры под
|
||||
`.deploy-state-<repo>/<wi>/`, `read_result`, `map_exit_code_to_status`).
|
||||
- Фоновый daemon-паттерн: `src/reconciler.py` (`threading.Thread(daemon=True)` +
|
||||
`threading.Event`, старт/стоп в `src/main.py::lifespan` после `worker.start()` /
|
||||
перед `worker.stop()`, `status()` в `GET /queue`).
|
||||
- Reserved-agent (детерминированный no-LLM job) паттерн: `deploy-finalizer` —
|
||||
перехват в `src/agents/launcher.py::launch_job` ДО `_spawn`, исполнение
|
||||
`stage_engine.run_deploy_finalizer`, отложенная постановка через
|
||||
`enqueue_job(..., available_at_delay_s=...)`.
|
||||
- Условность self-hosting: `src/qg/checks.py::is_self_hosting_repo`,
|
||||
`src/self_deploy.py::self_deploy_applies` (флаг + CSV-репо; пусто → только `orchestrator`).
|
||||
- Наблюдаемые эндпоинты прода (`src/main.py`): `GET /health`, `GET /status`, `GET /queue`.
|
||||
- API БД: `src/db.py::enqueue_job` (с `available_at_delay_s`), `get_db`,
|
||||
`update_task_stage`, `get_active_tasks_for_reconcile`.
|
||||
|
||||
## 2. Требуемые изменения
|
||||
|
||||
### 2.1. Новый leaf-модуль чистой логики наблюдения — `src/post_deploy.py` (новый)
|
||||
Контракт **never-raise** (по образцу `self_deploy.py` / `staging_verdict.py`).
|
||||
Чистые, юнит-тестируемые функции:
|
||||
- **Опрос сигналов:** функция, опрашивающая `/health` и ключевые эндпоинты
|
||||
(`/status`, `/queue`) прод-инстанса (base-url из config), возвращающая структуру
|
||||
с результатами (код ответа, ok-флаг, доля 5xx). Сеть/таймаут → консервативный
|
||||
результат, не исключение.
|
||||
- **Классификация деградации** (чистая, без сети): на вход — серия результатов
|
||||
опросов; на выход — вердикт `HEALTHY | DEGRADED` по порогам (BR-3):
|
||||
`≥ post_deploy_fail_threshold` последовательных провалов health ИЛИ доля 5xx
|
||||
выше `post_deploy_5xx_threshold` на окне. Эта функция — основной предмет
|
||||
юнит-тестов (детерминированная, как `compute_staging_verdict` в ORCH-061).
|
||||
- **Решение о реакции** (чистая): по `(repo, вердикт, политика)` → одно из
|
||||
`NONE | ROLLBACK | ALERT_ONLY`, с учётом self-hosting (BR-5).
|
||||
- **Запись артефакта** результата наблюдения (см. §2.5), best-effort.
|
||||
- Условность: хелпер `post_deploy_applies(repo)` (флаг + CSV-репо, пусто →
|
||||
только self-hosting), по образцу `self_deploy_applies` / `_merge_gate_applies`.
|
||||
|
||||
### 2.2. Оркестрация наблюдения (механизм — выбор архитектора)
|
||||
Требования к механизму (независимо от выбора стадия/watchdog/reserved-agent):
|
||||
- запускается ПОСЛЕ перехода `deploy → done` для применимого репозитория (BR-1);
|
||||
- наблюдает окно `post_deploy_window_s` с интервалом `post_deploy_interval_s`;
|
||||
- **restart-safe и идемпотентен** (BR-7): состояние наблюдения — в sentinel-файлах
|
||||
(по образцу `.deploy-state-<repo>/<wi>/`, напр. маркеры `monitor-started` /
|
||||
`monitor-done`) ИЛИ через отложенные `enqueue_job(available_at_delay_s=...)`;
|
||||
повторный старт не задваивает наблюдение и не теряет его при рестарте;
|
||||
- по итогу вызывает «Решение о реакции» из `src/post_deploy.py` и исполняет реакцию (§2.3).
|
||||
|
||||
Кандидатные точки интеграции (на выбор архитектора, см. BRD §7):
|
||||
- хук в `stage_engine.advance_stage` в блоке `next_stage == "done"` — арм наблюдения;
|
||||
- reserved-agent `post-deploy-monitor` (расширение `launcher.launch_job` ДО `_spawn`,
|
||||
как `deploy-finalizer`), с само-перепостановкой через `available_at_delay_s`;
|
||||
- отдельный daemon-поток `PostDeployWatcher` (как `Reconciler`), старт/стоп в `main.lifespan`.
|
||||
|
||||
### 2.3. Реакция на деградацию
|
||||
- **Не-self репозитории / политика auto:** вызвать существующий хук в режиме отката
|
||||
(`scripts/orchestrator-deploy-hook.sh --rollback` с прод-параметрами окружения,
|
||||
как в `self_deploy.build_deploy_command`, но action=`--rollback`). Маппинг
|
||||
exit-code хука (0/1/2) в исход переиспользует логику `self_deploy.map_exit_code_to_status`
|
||||
по смыслу (0 → откат успешен; 1/2 → откат не выполнен/провалился → громкий алерт).
|
||||
- **Self-hosting (`orchestrator`) по умолчанию (BR-5):** НЕ откатывать автоматически.
|
||||
Сформировать громкий алерт (Telegram + Plane-коммент) и запросить ручной approve
|
||||
отката (по образцу deploy Phase A — статус Plane / Telegram CTA). Откат самого
|
||||
прод-орка, если выполняется, — только через detached host-процесс (нельзя надёжно
|
||||
откатить контейнер, который при этом умирает; переиспользовать механику
|
||||
`self_deploy.initiate_deploy`).
|
||||
- Команда отката для self НЕ должна ронять прод-контейнер в рамках обычного тика
|
||||
наблюдения (CLAUDE.md: не ронять/не рестартить прод-контейнер вне явного действия).
|
||||
|
||||
### 2.4. Конфигурация — `src/config.py` (расширение `Settings`)
|
||||
Добавить (env-префикс `ORCH_`, дефолты безопасные):
|
||||
- `post_deploy_monitor_enabled: bool = True` — глобальный kill-switch (BR-8).
|
||||
- `post_deploy_repos: str = ""` — CSV применимых репо; пусто → только self-hosting
|
||||
(по образцу `self_deploy_repos` / `merge_gate_repos` / `image_freshness_repos`).
|
||||
- `post_deploy_window_s: int = 900` — длина окна наблюдения (дефолт ~15 мин, BR-1).
|
||||
- `post_deploy_interval_s: int = 30` — интервал между опросами.
|
||||
- `post_deploy_fail_threshold: int = 3` — N последовательных провалов health → DEGRADED.
|
||||
- `post_deploy_5xx_threshold: float = 0.5` — порог доли 5xx на окне → DEGRADED.
|
||||
- `post_deploy_auto_rollback: bool = False` — глобально разрешён ли авто-откат;
|
||||
при `True` действует для не-self репо; для self всегда требует approve (BR-5).
|
||||
- `post_deploy_base_url: str = "http://localhost:8500"` — base-url наблюдаемого прода.
|
||||
- `post_deploy_target` параметры отката — переиспользовать существующие
|
||||
`deploy_prod_*` (service/port/image/prev_image_file), новых дублей не вводить.
|
||||
|
||||
### 2.5. Артефакт задачи — `16-post-deploy-log.md` (новый)
|
||||
В `docs/work-items/<plane-id>/`. YAML-frontmatter (машиночитаемо, канон гейтов;
|
||||
для будущей петли уроков BR-10):
|
||||
```
|
||||
---
|
||||
post_deploy_status: HEALTHY | DEGRADED
|
||||
action_taken: NONE | ROLLBACK_OK | ROLLBACK_FAILED | ALERT_ONLY
|
||||
work_item: <plane-id>
|
||||
window_s: <int>
|
||||
checks_total: <int>
|
||||
checks_failed: <int>
|
||||
---
|
||||
```
|
||||
Тело — человекочитаемая сводка опросов. Записывается best-effort (по образцу
|
||||
`self_deploy.write_deploy_log`); отсутствие файла не должно ничего ронять.
|
||||
> Артефакт `16-post-deploy-log.md` добавить в перечень артефактов в `CLAUDE.md`
|
||||
> и таблицу/описание в `docs/architecture/README.md` (golden-source, в том же PR).
|
||||
|
||||
### 2.6. Наблюдаемость — `GET /queue` (`src/main.py`) (BR-9)
|
||||
Добавить блок `post_deploy` со снимком состояния (enabled, window, активные
|
||||
наблюдения, последний исход) — по образцу блока `reconcile` (метод `status()`).
|
||||
|
||||
### 2.7. Изменения схемы БД
|
||||
**Не требуются.** Состояние наблюдения — sentinel-файлы (restart-safe, без миграции,
|
||||
по образцу ORCH-36) и/или отложенные jobs. Если архитектор выберет колонку в `tasks`
|
||||
для отметки наблюдения — потребуется миграция; предпочтительно избежать (как ORCH-36/53/58).
|
||||
|
||||
### 2.8. Новые QG checks
|
||||
**Не требуются.** Наблюдение происходит ПОСЛЕ `done` и не является gate'ом стадии;
|
||||
реестр `QG_CHECKS` и `STAGE_TRANSITIONS` не меняются (если архитектор НЕ выберет
|
||||
вариант «отдельная пост-deploy стадия» — тогда потребуется новая стадия+gate, что
|
||||
надо явно отразить в ADR; по умолчанию предпочтителен вариант без изменения реестров).
|
||||
|
||||
## 3. Инварианты (НЕ ломать)
|
||||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, контракт `check_deploy_status` /
|
||||
`_parse_deploy_status`, момент вердикта `deploy_status`, БАГ-8 откат, terminal-sync
|
||||
`deploy → done`, merge-gate, exit-code-контракт хука (0/1/2) — без изменений.
|
||||
- Контракт хука: дефолты STAGING-безопасны; прод-параметры приходят только через env.
|
||||
- Условность как ORCH-35/36/43/58: реально для `orchestrator`/listed-repos, прочие — no-op.
|
||||
- Never-raise: ошибка в наблюдении не роняет worker / lifespan / конвейер других проектов.
|
||||
- Self-hosting: тик наблюдения НИКОГДА не рестартит прод-контейнер сам по себе (BR-5).
|
||||
|
||||
## 4. Задействованные модули (сводка)
|
||||
| Модуль | Изменение |
|
||||
|--------|-----------|
|
||||
| `src/post_deploy.py` | **новый** — чистая логика опроса/классификации/решения/артефакта, never-raise |
|
||||
| `src/config.py` | +параметры `post_deploy_*` (kill-switch, окно, пороги, политика) |
|
||||
| `src/stage_engine.py` и/или `src/agents/launcher.py` и/или `src/main.py` | арм/исполнение наблюдения (точка — за архитектором) |
|
||||
| `scripts/orchestrator-deploy-hook.sh` | переиспользуется (`--rollback`); правки — только если откат self требует отдельной ветки (за архитектором) |
|
||||
| `src/main.py` | блок `post_deploy` в `GET /queue` (BR-9); возможный старт daemon в `lifespan` |
|
||||
| `docs/work-items/<id>/16-post-deploy-log.md` | **новый** артефакт |
|
||||
| `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md` | обновить (golden-source, в том же PR) |
|
||||
| ADR | `docs/work-items/ORCH-021/06-adr/ADR-001-*.md` (+ возможный сквозной `adr/adr-00NN`) |
|
||||
|
||||
## 5. Артефакты по pipeline, которые должны появиться/обновиться
|
||||
- `16-post-deploy-log.md` (новый, машиночитаемый frontmatter).
|
||||
- Обновлённые `CLAUDE.md` (перечень артефактов), `docs/architecture/README.md`
|
||||
(описание пост-деплой наблюдения), `CHANGELOG.md`.
|
||||
- ADR work-item (`06-adr/`) с зафиксированным выбором механизма и порогов.
|
||||
106
docs/work-items/ORCH-021/03-acceptance-criteria.md
Normal file
106
docs/work-items/ORCH-021/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Критерии приёмки — ORCH-021
|
||||
|
||||
Work Item: ORCH-021
|
||||
Формат: каждый критерий имеет чёткое условие PASS/FAIL и проверяется тестом
|
||||
из `04-test-plan.yaml`.
|
||||
|
||||
## Наблюдение и сигналы
|
||||
|
||||
### AC-1 — наблюдение армится после deploy→done
|
||||
- **PASS:** для применимого репозитория после терминального перехода `deploy → done`
|
||||
пост-деплой наблюдение инициируется (создаётся sentinel/отложенный job/запись в watcher).
|
||||
- **FAIL:** переход `deploy → done` не приводит к старту наблюдения.
|
||||
|
||||
### AC-2 — наблюдение НЕ армится для неприменимых репо
|
||||
- **PASS:** для репозитория вне области (не self-hosting и не в `post_deploy_repos`)
|
||||
`post_deploy_applies(repo)` → False; наблюдение не стартует; конвейер не меняется.
|
||||
- **FAIL:** наблюдение стартует для неприменимого репо.
|
||||
|
||||
### AC-3 — классификация HEALTHY
|
||||
- **PASS:** серия опросов без провалов (или провалов меньше `post_deploy_fail_threshold`
|
||||
и доля 5xx ниже `post_deploy_5xx_threshold`) → вердикт `HEALTHY`.
|
||||
- **FAIL:** при здоровых сигналах возвращается `DEGRADED`.
|
||||
|
||||
### AC-4 — классификация DEGRADED по порогу провалов health
|
||||
- **PASS:** `≥ post_deploy_fail_threshold` ПОСЛЕДОВАТЕЛЬНЫХ провалов health → `DEGRADED`.
|
||||
- **FAIL:** порог достигнут, но вердикт не `DEGRADED`.
|
||||
|
||||
### AC-5 — классификация DEGRADED по доле 5xx
|
||||
- **PASS:** доля 5xx на окне выше `post_deploy_5xx_threshold` → `DEGRADED`,
|
||||
даже если `/health` отвечает 200.
|
||||
- **FAIL:** превышение порога 5xx не даёт `DEGRADED`.
|
||||
|
||||
### AC-6 — устойчивость к разовому глюку (нет ложного срабатывания)
|
||||
- **PASS:** одиночный провал (1 < `post_deploy_fail_threshold`) с последующим
|
||||
восстановлением → итог `HEALTHY`, реакции нет.
|
||||
- **FAIL:** одиночный разовый провал приводит к `DEGRADED`/откату.
|
||||
|
||||
## Реакция
|
||||
|
||||
### AC-7 — авто-rollback для не-self репо при политике auto
|
||||
- **PASS:** при `post_deploy_auto_rollback=True` и НЕ-self репо вердикт `DEGRADED`
|
||||
приводит к вызову отката (хук `--rollback` с прод-параметрами); `action_taken`
|
||||
фиксируется как `ROLLBACK_OK`/`ROLLBACK_FAILED` по exit-code.
|
||||
- **FAIL:** откат не вызывается, либо вызывается с staging-дефолтами, либо роняет прод напрямую.
|
||||
|
||||
### AC-8 — self-hosting НЕ откатывается автоматически (safety)
|
||||
- **PASS:** для `orchestrator` вердикт `DEGRADED` НЕ приводит к автоматическому
|
||||
откату/рестарту прод-контейнера в тике наблюдения; вместо этого формируется
|
||||
громкий алерт + запрос ручного approve (`action_taken: ALERT_ONLY`).
|
||||
- **FAIL:** тик наблюдения автоматически откатывает/рестартит прод-орк.
|
||||
|
||||
### AC-9 — откат-провал эскалируется
|
||||
- **PASS:** если откат вызван и вернул код 1/2 (нет prev-образа / откат тоже упал) →
|
||||
`action_taken: ROLLBACK_FAILED` + громкий Telegram-алерт о необходимости ручного вмешательства.
|
||||
- **FAIL:** провал отката проглатывается тихо.
|
||||
|
||||
## Конфигурация и совместимость
|
||||
|
||||
### AC-10 — kill-switch выключает фичу
|
||||
- **PASS:** `post_deploy_monitor_enabled=False` → наблюдение не армится ни для кого;
|
||||
поведение конвейера 1:1 как до ORCH-021.
|
||||
- **FAIL:** при выключенном флаге наблюдение всё равно работает.
|
||||
|
||||
### AC-11 — пороги/окно конфигурируемы через env
|
||||
- **PASS:** `post_deploy_window_s`, `post_deploy_interval_s`, `post_deploy_fail_threshold`,
|
||||
`post_deploy_5xx_threshold` читаются из `Settings` (env `ORCH_*`) и влияют на поведение.
|
||||
- **FAIL:** значения захардкожены.
|
||||
|
||||
### AC-12 — реестры и схема БД не изменены
|
||||
- **PASS:** `STAGE_TRANSITIONS`, `QG_CHECKS`, контракт `check_deploy_status` и схема
|
||||
таблиц БД не изменены (если архитектор не вводит явно новую стадию — тогда это
|
||||
отражено в ADR и тестах). Существующие тесты deploy/staging/merge-gate зелёные.
|
||||
- **FAIL:** молча сломан какой-либо существующий контракт/тест.
|
||||
|
||||
## Наблюдаемость, артефакт, идемпотентность
|
||||
|
||||
### AC-13 — артефакт 16-post-deploy-log.md с машиночитаемым frontmatter
|
||||
- **PASS:** по итогу наблюдения пишется `16-post-deploy-log.md` с валидным YAML-frontmatter
|
||||
(`post_deploy_status`, `action_taken`); запись best-effort (её отсутствие ничего не роняет).
|
||||
- **FAIL:** артефакт не пишется или frontmatter невалиден/непарсится.
|
||||
|
||||
### AC-14 — наблюдаемость в /queue
|
||||
- **PASS:** `GET /queue` содержит блок `post_deploy` со снимком состояния (enabled,
|
||||
window, активные/последний исход).
|
||||
- **FAIL:** состояние наблюдения нигде не видно.
|
||||
|
||||
### AC-15 — идемпотентность / restart-safe
|
||||
- **PASS:** повторный арм для той же задачи (двойной webhook / рестарт оркестратора)
|
||||
не создаёт второе параллельное наблюдение и не теряет уже идущее.
|
||||
- **FAIL:** дублируется наблюдение или теряется при рестарте.
|
||||
|
||||
### AC-16 — never-raise
|
||||
- **PASS:** любая ошибка опроса/сети/файлов/классификации логируется и НЕ роняет
|
||||
worker / lifespan / конвейер других проектов.
|
||||
- **FAIL:** исключение из наблюдения всплывает и ломает обслуживание других проектов.
|
||||
|
||||
### AC-17 — уведомления
|
||||
- **PASS:** ключевые события (наблюдение начато, DEGRADED, откат/алерт, чистое
|
||||
завершение окна) уведомляются в Telegram и/или Plane-комментарием.
|
||||
- **FAIL:** деградация/откат происходят молча.
|
||||
|
||||
### AC-18 — документация обновлена (golden-source)
|
||||
- **PASS:** в том же PR обновлены `CLAUDE.md` (артефакт `16-post-deploy-log.md`),
|
||||
`docs/architecture/README.md` (описание пост-деплой наблюдения), `CHANGELOG.md`,
|
||||
и заведён ADR work-item.
|
||||
- **FAIL:** функционал есть, документация не обновлена (reviewer → REQUEST_CHANGES).
|
||||
163
docs/work-items/ORCH-021/04-test-plan.yaml
Normal file
163
docs/work-items/ORCH-021/04-test-plan.yaml
Normal file
@@ -0,0 +1,163 @@
|
||||
work_item: ORCH-021
|
||||
description: >
|
||||
Тест-план пост-деплой мониторинга прода + авто-rollback. Упор на детерминированную
|
||||
чистую логику классификации/решения (юнит, без сети/LLM) и на интеграцию
|
||||
армирования наблюдения после deploy->done. Сетевые опросы и хук-вызовы мокируются.
|
||||
Имена модулей/функций — целевые (src/post_deploy.py); архитектор уточняет точную
|
||||
сигнатуру, тесты адаптируются под ADR.
|
||||
|
||||
tests:
|
||||
# --- Классификация деградации (чистая логика, ядро) ---
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "HEALTHY: серия опросов без провалов (< порога) -> вердикт HEALTHY"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-3]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: "DEGRADED: N последовательных провалов health (== fail_threshold) -> DEGRADED"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-4]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: "DEGRADED по 5xx: доля 5xx выше порога при health=200 -> DEGRADED"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-5]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: "Нет ложного срабатывания: одиночный провал (1 < threshold) + восстановление -> HEALTHY"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-6]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: "Пороги читаются из Settings (env ORCH_*), изменение порога меняет вердикт на тех же данных"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-11]
|
||||
expected: PASS
|
||||
|
||||
# --- Решение о реакции (чистая логика + self-hosting safety) ---
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: "Решение: не-self репо + auto_rollback=True + DEGRADED -> ROLLBACK"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-7]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: "Решение self-hosting: orchestrator + DEGRADED -> ALERT_ONLY (НИКОГДА не авто-rollback)"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-8]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: "Решение: HEALTHY -> NONE (реакции нет) для любого репо"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-3]
|
||||
expected: PASS
|
||||
|
||||
# --- Условность / kill-switch ---
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: "post_deploy_applies: пусто в repos -> True только для orchestrator, False для enduro-trails"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-2]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: "kill-switch: post_deploy_monitor_enabled=False -> applies()=False для всех; наблюдение не армится"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-10]
|
||||
expected: PASS
|
||||
|
||||
# --- Маппинг exit-code отката -> исход ---
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: "Откат exit 0 -> action_taken=ROLLBACK_OK"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-7]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: "Откат exit 1/2 (нет prev-образа / откат упал) -> ROLLBACK_FAILED + эскалация-алерт"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-9]
|
||||
expected: PASS
|
||||
|
||||
# --- Артефакт ---
|
||||
- id: TC-13
|
||||
type: unit
|
||||
description: "16-post-deploy-log.md пишется с валидным YAML-frontmatter (post_deploy_status/action_taken), парсится yaml.safe_load"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-13]
|
||||
expected: PASS
|
||||
|
||||
# --- never-raise ---
|
||||
- id: TC-14
|
||||
type: unit
|
||||
description: "Опрос при сетевой ошибке/таймауте -> консервативный результат (провал-как-down), исключение НЕ всплывает"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-16]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-15
|
||||
type: unit
|
||||
description: "Ошибка записи артефакта (нет каталога/IO) -> логируется, функция возвращает False, не raise"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-16, AC-13]
|
||||
expected: PASS
|
||||
|
||||
# --- Интеграция: армирование после deploy->done ---
|
||||
- id: TC-16
|
||||
type: integration
|
||||
description: "advance_stage deploy->done для orchestrator армит наблюдение (sentinel/job создан); для enduro-trails — нет"
|
||||
module: tests/test_post_deploy_integration.py
|
||||
covers: [AC-1, AC-2]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-17
|
||||
type: integration
|
||||
description: "Идемпотентность: повторный арм той же задачи (двойной webhook) не создаёт второе наблюдение"
|
||||
module: tests/test_post_deploy_integration.py
|
||||
covers: [AC-15]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-18
|
||||
type: integration
|
||||
description: "Полный цикл DEGRADED -> для не-self вызывается откат (хук замокан), пишется лог, шлётся уведомление"
|
||||
module: tests/test_post_deploy_integration.py
|
||||
covers: [AC-7, AC-13, AC-17]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-19
|
||||
type: integration
|
||||
description: "Self-hosting DEGRADED: тик НЕ вызывает рестарт/откат прод-контейнера, формирует алерт+approve-запрос"
|
||||
module: tests/test_post_deploy_integration.py
|
||||
covers: [AC-8, AC-17]
|
||||
expected: PASS
|
||||
|
||||
# --- Наблюдаемость и обратная совместимость ---
|
||||
- id: TC-20
|
||||
type: integration
|
||||
description: "GET /queue содержит блок post_deploy со снимком состояния"
|
||||
module: tests/test_post_deploy_integration.py
|
||||
covers: [AC-14]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-21
|
||||
type: integration
|
||||
description: "Регресс: существующие тесты deploy/staging/merge-gate/reconciler зелёные; STAGE_TRANSITIONS и QG_CHECKS не изменены"
|
||||
module: tests/test_stages.py
|
||||
covers: [AC-12]
|
||||
expected: PASS
|
||||
212
docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md
Normal file
212
docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# ADR-001 (ORCH-021): Post-deploy мониторинг прода + реакция на деградацию
|
||||
|
||||
## Статус
|
||||
Proposed (design) — реализация в ветке `feature/ORCH-021-post-deploy-rollback`.
|
||||
Сквозной индексный ADR: `docs/architecture/adr/adr-0010-post-deploy-monitor.md`.
|
||||
Помечено `arch:major-change` (новая под-компонента + новый reserved-agent job-kind).
|
||||
|
||||
## Контекст
|
||||
Конвейер заканчивается на `deploy → done` (`check_deploy_status` видит
|
||||
`deploy_status: SUCCESS` → terminal-sync, Plane → Done, release merge-lease). После
|
||||
этого оркестратор **забывает про прод**. «Успех» сегодня = прохождение health-check
|
||||
в момент рестарта (10×6с в `scripts/orchestrator-deploy-hook.sh`) — узкое окно ~60с.
|
||||
|
||||
Класс инцидентов «зелёный деплой, красный прод» (прецедент **ET-8**): деградация
|
||||
проявляется через минуты под боевым трафиком (прогрев кэшей, фоновые миграции,
|
||||
утечки, рост 5xx), health отвечает `200 ok`, но фича сломана. Для self-hosting это
|
||||
критично: сломанный прод-орк (8500) обслуживает ВСЕ проекты из общего инстанса.
|
||||
|
||||
BRD/ТЗ задают требования (BR-1…BR-11, AC-1…AC-18) и оставляют архитектору **три
|
||||
открытых вопроса** (BRD §7): (1) где живёт наблюдение — стадия / watchdog-daemon /
|
||||
reserved-agent job; (2) механизм self-rollback; (3) пороги/веса сигналов.
|
||||
|
||||
Существующие переиспользуемые механики:
|
||||
- **deploy-finalizer** (ORCH-36, `stage_engine.run_deploy_finalizer` + перехват в
|
||||
`launcher.launch_job` ДО `_spawn`) — детерминированный no-LLM reserved-agent job,
|
||||
само-перепостановка через `enqueue_job(available_at_delay_s=...)`, defer-budget,
|
||||
restart-safe (jobs-очередь + sentinel-файлы `.deploy-state-<repo>/<wi>/`).
|
||||
- **self_deploy.py** — sentinel-state хелперы (`write_marker`/`has_marker`/
|
||||
`read_result`/`clear_state`), detached host-процесс (`build_deploy_command`/
|
||||
`initiate_deploy`: ssh + setsid), `map_exit_code_to_status`, `self_deploy_applies`.
|
||||
- **reconciler.py** — daemon-поток + `status()` в `GET /queue`.
|
||||
- **хук `--rollback`** (`do_rollback`): retag `PREV_IMAGE_FILE` → `TARGET_IMAGE` +
|
||||
рестарт + health, коды 0 / 1 (нет prev-образа) / 2 (rollback тоже упал).
|
||||
- **Условность** ORCH-35/36/43/58: `is_self_hosting_repo`, флаг + CSV-репо.
|
||||
|
||||
## Решение
|
||||
|
||||
### 1. Механизм наблюдения — reserved-agent job `post-deploy-monitor` (Вариант B)
|
||||
Наблюдение реализуется как **детерминированный no-LLM reserved-agent job**, точная
|
||||
калька **deploy-finalizer**. Один «тик» наблюдения = один job: он делает ОДИН опрос
|
||||
сигналов, обновляет персистентные счётчики в sentinel-файлах, классифицирует и либо
|
||||
**перепостанавливает себя** с задержкой `post_deploy_interval_s` (окно не истекло и
|
||||
ещё не DEGRADED), либо завершает наблюдение (DEGRADED → реакция; либо окно истекло →
|
||||
HEALTHY). Это «watchdog поверх очереди»: между тиками job не выполняется (он
|
||||
запланирован в будущем через `available_at_delay_s`), worker свободен для других
|
||||
проектов — ровно как defer у finalizer.
|
||||
|
||||
**Почему НЕ daemon-watchdog (Вариант A, как reconciler):** daemon тикает глобально, а
|
||||
не per-task; серию опросов (последовательные провалы health, доля 5xx на окне) пришлось
|
||||
бы держать в памяти → теряется/двоится при рестарте (а сам деплой орка = рестарт). Чтобы
|
||||
сделать daemon restart-safe, всё равно нужны персистентные per-task счётчики в sentinel —
|
||||
тогда reserved-agent проще и уже имеет проверенную restart-safe машинерию (jobs-очередь
|
||||
+ `requeue_running_jobs` + sentinels). Per-task жизненный цикл естественно ложится на
|
||||
job-цепочку, а не на глобальный sweep.
|
||||
|
||||
**Почему НЕ отдельная пост-deploy стадия (Вариант C):** меняет `STAGE_TRANSITIONS` +
|
||||
реестр `QG_CHECKS` (нарушает AC-12, ТЗ §2.8 — явно непредпочтительно); ломает семантику
|
||||
`deploy → done` как терминального перехода (Plane уже Done). Наблюдение происходит
|
||||
**ПОСЛЕ** `done` — «продление ответственности ЗА done», а не новая стадия конвейера.
|
||||
|
||||
### 2. Арм наблюдения — хук в terminal-блоке `advance_stage`
|
||||
В `stage_engine.advance_stage`, в существующем блоке `next_stage == "done"` (после
|
||||
`set_issue_done` и `release_merge_lease`), добавляется арм:
|
||||
```
|
||||
if next_stage == "done" and post_deploy.post_deploy_applies(repo):
|
||||
post_deploy.arm_monitor(repo, work_item_id, branch, task_id)
|
||||
```
|
||||
`arm_monitor` (never-raise): если sentinel `armed` отсутствует → создаёт state-dir,
|
||||
пишет `armed` (идемпотентность, по образцу `INITIATED`), инициализирует `series`-файл,
|
||||
ставит первый `post-deploy-monitor` job через `enqueue_job(available_at_delay_s=
|
||||
post_deploy_interval_s)`. Если `armed` уже есть → no-op (двойной webhook / reconciler
|
||||
F-1 / finalizer Phase C могут довести `done` повторно — AC-15). Выключенный
|
||||
kill-switch / неприменимый репо → `post_deploy_applies` False → арма нет (AC-2/AC-10).
|
||||
|
||||
### 3. Чистая логика — новый leaf-модуль `src/post_deploy.py` (never-raise)
|
||||
По образцу `self_deploy.py` / `staging_verdict.py`. Импортирует только config (+lazy
|
||||
`qg.checks.is_self_hosting_repo`), НЕ импортирует `stage_engine`/`launcher`. Функции:
|
||||
- **`post_deploy_applies(repo) -> bool`** — флаг `post_deploy_monitor_enabled` +
|
||||
CSV `post_deploy_repos` (пусто → только self-hosting). Калька `self_deploy_applies`.
|
||||
- **`probe_signals(base_url) -> ProbeResult`** — один опрос: `GET /health` (HTTP 200 +
|
||||
`{"status":"ok"}`) и ключевые эндпоинты `/status`, `/queue` (учёт доли 5xx).
|
||||
Сеть/таймаут → консервативный «провал»-результат, не исключение.
|
||||
- **`classify(series, fail_threshold, 5xx_threshold) -> "HEALTHY"|"DEGRADED"`** —
|
||||
чистая, без сети, **главный предмет юнит-тестов** (детерминированная, как
|
||||
`compute_staging_verdict`): `DEGRADED` если `≥ fail_threshold` ПОСЛЕДОВАТЕЛЬНЫХ
|
||||
провалов health (AC-4) ИЛИ доля 5xx на окне `> 5xx_threshold` (AC-5). Иначе
|
||||
`HEALTHY` (одиночный провал < порога с восстановлением → HEALTHY, AC-3/AC-6).
|
||||
- **`decide_action(repo, verdict) -> "NONE"|"ROLLBACK"|"ALERT_ONLY"`** — чистая:
|
||||
`HEALTHY → NONE`; `DEGRADED` + self-hosting → `ALERT_ONLY` (BR-5/AC-8, ВСЕГДА);
|
||||
`DEGRADED` + не-self + `post_deploy_auto_rollback=True` → `ROLLBACK`; иначе →
|
||||
`ALERT_ONLY`.
|
||||
- **Sentinel-state хелперы** (state-dir `.post-deploy-state-<repo>/<wi>/`, по образцу
|
||||
`self_deploy._state_dir`): `armed`, `series` (JSON-список результатов опросов,
|
||||
append каждый тик — restart-safe счётчики), `done`. `read_series`/`append_probe`/
|
||||
`mark_done`/`has_marker` — never-raise.
|
||||
- **`write_post_deploy_log(...)`** — артефакт `16-post-deploy-log.md`, best-effort
|
||||
(по образцу `self_deploy.write_deploy_log`).
|
||||
- **`build_rollback_command(repo)`** — argv хука `--rollback` с прод-env (как
|
||||
`build_deploy_command`, но action=`--rollback`; переиспользует `deploy_prod_*`).
|
||||
|
||||
### 4. Исполнение тика — `stage_engine.run_post_deploy_monitor(job)` + перехват в launcher
|
||||
По образцу `run_deploy_finalizer` / `_run_deploy_finalizer_job`:
|
||||
`launcher.launch_job` перехватывает `agent == "post-deploy-monitor"` ДО `_spawn` →
|
||||
`stage_engine.run_post_deploy_monitor(job)`. Алгоритм тика (never-raise):
|
||||
1. `mark_done` уже стоит → no-op (AC-15, защита от дубля).
|
||||
2. `probe = post_deploy.probe_signals(base_url)`; `append_probe(series, probe)`.
|
||||
3. `verdict = classify(series, ...)`.
|
||||
4. **Если `HEALTHY` и окно не истекло** (число тиков < `window_s/interval_s`) →
|
||||
перепостановка `post-deploy-monitor` через `available_at_delay_s=interval_s`
|
||||
(как finalizer defer; счётчик тиков — из jobs-очереди/`series`, restart-safe).
|
||||
5. **Если `HEALTHY` и окно истекло** → исход `NONE`, `write_post_deploy_log(HEALTHY,
|
||||
NONE)`, `mark_done`, нотификация «окно завершилось чисто» (BR-6/AC-17).
|
||||
6. **Если `DEGRADED`** → `action = decide_action(...)`; исполнить реакцию (§5),
|
||||
`write_post_deploy_log`, `mark_done`, нотификации.
|
||||
|
||||
`mark_done` + sentinel `armed` дают идемпотентность; jobs-очередь +
|
||||
`requeue_running_jobs` + `series` дают restart-safe (AC-15). Бюджет тиков bounded
|
||||
(`window_s/interval_s`) — анти-livelock, как `deploy_finalize_max_attempts`.
|
||||
|
||||
### 5. Реакция на деградацию
|
||||
- **Self-hosting (`orchestrator`), всегда (BR-5/AC-8):** `ALERT_ONLY`. НЕ откатывать
|
||||
и НЕ рестартить прод-контейнер в тике. Громкий Telegram + Plane-коммент с запросом
|
||||
ручного approve отката (по образцу deploy Phase A CTA). `action_taken: ALERT_ONLY`.
|
||||
Откат самого прод-орка (если оператор решит) — ТОЛЬКО через detached host-процесс
|
||||
(контейнер не откатит себя, умирая); переиспользуется механика
|
||||
`self_deploy.initiate_deploy`, но в MVP она вне тика наблюдения (ручной approve →
|
||||
отдельный путь, как ORCH-54 для авто-deploy). Тик self НИКОГДА не запускает хук
|
||||
`--rollback` (структурный инвариант).
|
||||
- **Не-self + `post_deploy_auto_rollback=True` (AC-7):** вызвать хук `--rollback` с
|
||||
прод-env (`build_rollback_command`). Маппинг exit-code по смыслу
|
||||
`map_exit_code_to_status`: `0 → ROLLBACK_OK`; `1/2 → ROLLBACK_FAILED` + громкий
|
||||
Telegram о необходимости ручного вмешательства (AC-9). Целевой контейнер не есть
|
||||
orchestrator → его рестарт безопасен для конвейера.
|
||||
- **Не-self + auto_rollback=False (дефолт):** `ALERT_ONLY`.
|
||||
|
||||
### 6. Артефакт `16-post-deploy-log.md` (новый, машиночитаемый)
|
||||
YAML-frontmatter (канон гейтов; для петли уроков ORCH-8, BR-10):
|
||||
```
|
||||
---
|
||||
post_deploy_status: HEALTHY | DEGRADED
|
||||
action_taken: NONE | ROLLBACK_OK | ROLLBACK_FAILED | ALERT_ONLY
|
||||
work_item: <plane-id>
|
||||
window_s: <int>
|
||||
checks_total: <int>
|
||||
checks_failed: <int>
|
||||
---
|
||||
```
|
||||
Тело — человекочитаемая сводка опросов. Best-effort (отсутствие файла ничего не роняет,
|
||||
AC-13). **Не** читается ни одним гейтом — наблюдение происходит после `done`.
|
||||
|
||||
### 7. Конфигурация — `src/config.py` (env-префикс `ORCH_`)
|
||||
- `post_deploy_monitor_enabled: bool = True` — глобальный kill-switch (BR-8/AC-10).
|
||||
- `post_deploy_repos: str = ""` — CSV применимых репо; пусто → только self-hosting.
|
||||
- `post_deploy_window_s: int = 900` — окно наблюдения (~15 мин, BR-1).
|
||||
- `post_deploy_interval_s: int = 30` — интервал опросов.
|
||||
- `post_deploy_fail_threshold: int = 3` — N послед. провалов health → DEGRADED.
|
||||
- `post_deploy_5xx_threshold: float = 0.5` — порог доли 5xx → DEGRADED.
|
||||
- `post_deploy_auto_rollback: bool = False` — глоб. разрешение авто-отката (для self
|
||||
всегда требует approve, BR-5).
|
||||
- `post_deploy_base_url: str = "http://localhost:8500"` — наблюдаемый прод.
|
||||
- Параметры отката — переиспользовать существующие `deploy_prod_*` (новых дублей нет).
|
||||
|
||||
### 8. Наблюдаемость — блок `post_deploy` в `GET /queue` (BR-9/AC-14)
|
||||
По образцу блока `reconcile` (метод `status()`): `enabled`, `window_s`, `interval_s`,
|
||||
активные наблюдения (по sentinel `armed` без `done`), последний исход
|
||||
(`post_deploy_status`/`action_taken`). Best-effort, never-raise.
|
||||
|
||||
### Инварианты (НЕ меняются)
|
||||
`STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_deploy_status`/`_parse_deploy_status`,
|
||||
момент вердикта `deploy_status`, БАГ-8 откат, terminal-sync `deploy → done`, merge-gate,
|
||||
exit-code-контракт хука (0/1/2), схема БД. Условность как ORCH-35/36/43/58. Never-raise
|
||||
во всём наблюдении (AC-16). Тик self НИКОГДА не рестартит прод-контейнер (AC-8).
|
||||
|
||||
## Альтернативы
|
||||
- **Daemon-watchdog (как reconciler)** — отклонён: per-task серия в памяти не
|
||||
restart-safe; restart-safe-вариант требует тех же sentinel-счётчиков → reserved-agent
|
||||
проще и уже проверен.
|
||||
- **Отдельная пост-deploy стадия + QG** — отклонён: меняет реестры (AC-12), ломает
|
||||
семантику терминального `done`; наблюдение принципиально ПОСЛЕ `done`.
|
||||
- **Авто-rollback прод-орка из тика** — отклонён (BR-5): контейнер не откатит себя
|
||||
надёжно; групповой риск для всех проектов. Self → только ALERT + ручной approve.
|
||||
- **Новая колонка в `tasks` для отметки наблюдения** — отклонён: миграция на проде
|
||||
(риск, как в adr-0007); sentinel-файлы достаточны и restart-safe (как ORCH-36/53/58).
|
||||
- **Прометей/APM** — вне рамок (BR out-of-scope): опираемся на существующие
|
||||
HTTP-эндпоинты, не вводим сбор метрик.
|
||||
|
||||
## Последствия
|
||||
- Класс «зелёный деплой, красный прод» закрыт измеримыми порогами; деградация —
|
||||
машиночитаемый сигнал для петли уроков (ORCH-8).
|
||||
- Плюс: максимальное переиспользование проверенной finalizer/sentinel/hook-машинерии;
|
||||
нулевая миграция БД; реестры не тронуты; дефолты безопасны (auto-rollback off, self
|
||||
только alert).
|
||||
- Минус/ограничение: монитор self бежит ВНУТРИ наблюдаемого прод-контейнера — если
|
||||
контейнер полностью wedged, worker может не выполнить тик и алерта не будет (gap).
|
||||
Это known limitation MVP; внешний независимый watchdog — follow-up (вне рамок).
|
||||
- Минус: каждый тик на короткое время занимает single-worker (`max_concurrency=1`);
|
||||
митигируется коротким опросом (~секунды) и `interval_s` между тиками (defer не держит
|
||||
worker), как finalizer.
|
||||
- Доменный smoke результата фичи (BR-11) — follow-up; MVP = health + 5xx.
|
||||
|
||||
## Связи
|
||||
- **ET-8** — обоснование (deploy SUCCESS, прод не работает).
|
||||
- **adr-0007-executable-self-deploy** (ORCH-36) — sentinel-паттерн, detached
|
||||
host-процесс, `map_exit_code_to_status`, deploy-finalizer reserved-agent (образец).
|
||||
- **adr-0007-reconciler** (ORCH-53) — daemon/`status()` образец (рассмотрен и отклонён
|
||||
как основной механизм; `status()`-снимок в `/queue` переиспользуется).
|
||||
- **adr-0006-merge-gate** / **adr-0003-staging-gate** — образец условности и флагов
|
||||
раската (`*_enabled` + `*_repos`).
|
||||
- **adr-0008-staging-image-provenance** — `.deploy-prev-image` / хук-механика отката.
|
||||
- **ORCH-8** — петля уроков (потребитель `16-post-deploy-log.md`).
|
||||
- **ORCH-54** — будущий полный авто (включая авто-approve отката self), по аналогии
|
||||
с авто-deploy.
|
||||
56
docs/work-items/ORCH-021/07-infra-requirements.md
Normal file
56
docs/work-items/ORCH-021/07-infra-requirements.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 07 — Инфраструктурные требования (ORCH-021)
|
||||
|
||||
> Топология НЕ меняется. Фича опирается на уже существующие HTTP-эндпоинты прода и
|
||||
> существующий деплой-хук. Этот документ фиксирует, какие инфра-предпосылки должны
|
||||
> выполняться, чтобы наблюдение и реакция работали.
|
||||
|
||||
## 1. Топология — без изменений
|
||||
- Прод `orchestrator` (8500), staging `orchestrator-staging` (8501), один сервер
|
||||
mva154 (см. `docs/operations/INFRA.md`). Новых контейнеров/портов/сервисов нет.
|
||||
- Наблюдение — внутрипроцессный reserved-agent job в worker'е прод-контейнера.
|
||||
Daemon-потоков не добавляется (в отличие от reconciler).
|
||||
|
||||
## 2. Наблюдаемый прод — HTTP-эндпоинты
|
||||
- Монитор опрашивает `post_deploy_base_url` (дефолт `http://localhost:8500`):
|
||||
- `GET /health` → ожидается HTTP 200 + тело `{"status":"ok"}` (BR-2);
|
||||
- `GET /status`, `GET /queue` → учёт доли HTTP 5xx (BR-2).
|
||||
- Эндпоинты уже существуют (`src/main.py`). Новых эндпоинтов фича НЕ вводит
|
||||
(out-of-scope APM/метрики).
|
||||
- Для self-hosting `base_url=localhost:8500` означает: монитор бьёт по собственному
|
||||
контейнеру. Это допустимо для MVP (см. риск R-1 в `10-tech-risks.md`).
|
||||
|
||||
## 3. Деплой-хук `--rollback` — предпосылки реакции
|
||||
- Реакция ROLLBACK (только не-self + `post_deploy_auto_rollback=True`) вызывает
|
||||
`scripts/orchestrator-deploy-hook.sh --rollback` с прод-env (переиспользуются
|
||||
`deploy_prod_*`: `TARGET_SERVICE`/`TARGET_PORT`/`TARGET_IMAGE`/`COMPOSE_PROFILE`/
|
||||
`PREV_IMAGE_FILE`), по образцу `self_deploy.build_deploy_command`.
|
||||
- Предпосылка: при штатном деплое хук сохраняет предыдущий образ в
|
||||
`PREV_IMAGE_FILE` (`.deploy-prev-image-prod`). Без снимка → хук вернёт exit 1
|
||||
(«нет prev-образа») → `ROLLBACK_FAILED` + алерт (AC-9). Контракт exit-кодов хука
|
||||
(0/1/2) НЕ меняется.
|
||||
- **Self-hosting:** откат прод-орка хуком в тике ЗАПРЕЩЁН (контейнер не откатит себя,
|
||||
умирая). Если оператор по алерту решит откатить — только detached host-процесс
|
||||
(ssh + setsid, механика `self_deploy.initiate_deploy`), как у Phase B самодеплоя.
|
||||
Предпосылки для detached-пути (ssh-доступ host, shared-mount state-dir) уже
|
||||
выполнены для ORCH-36; в MVP detached-откат self вне тика наблюдения.
|
||||
|
||||
## 4. Restart-safe состояние — shared mount
|
||||
- Состояние наблюдения — sentinel-файлы под `.post-deploy-state-<repo>/<wi>/`
|
||||
(`armed`, `series`, `done`) на том же mount `settings.repos_dir`, что и
|
||||
`.deploy-state-*` (ORCH-36). Миграции БД нет (см. `08-data-requirements.md`).
|
||||
- `requeue_running_jobs` (ORCH-1) восстанавливает claimed `post-deploy-monitor` job
|
||||
после рестарта; `series` хранит счётчики опросов → наблюдение продолжается
|
||||
с того же места (BR-7/AC-15).
|
||||
|
||||
## 5. Конфигурация окружения (env `ORCH_*`)
|
||||
Новые ключи (дефолты безопасны, в `.env`/`.env.staging` по необходимости):
|
||||
`post_deploy_monitor_enabled` (kill-switch, дефолт true), `post_deploy_repos` (CSV,
|
||||
пусто → self-hosting), `post_deploy_window_s` (900), `post_deploy_interval_s` (30),
|
||||
`post_deploy_fail_threshold` (3), `post_deploy_5xx_threshold` (0.5),
|
||||
`post_deploy_auto_rollback` (false), `post_deploy_base_url` (localhost:8500).
|
||||
Параметры отката — существующие `deploy_prod_*`, новых дублей не вводить.
|
||||
|
||||
## 6. Чего НЕ требуется
|
||||
- Новых контейнеров, портов, сетевых правил, секретов.
|
||||
- Prometheus / Grafana / APM (out-of-scope).
|
||||
- Изменений compose-топологии или деплой-пути не-self репо.
|
||||
40
docs/work-items/ORCH-021/08-data-requirements.md
Normal file
40
docs/work-items/ORCH-021/08-data-requirements.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 08 — Требования к данным / схеме БД (ORCH-021)
|
||||
|
||||
## Вывод: миграция БД НЕ требуется
|
||||
Состояние наблюдения хранится в **sentinel-файлах** (restart-safe, без миграции —
|
||||
по образцу ORCH-36/53/58), а не в таблицах. Реестры и схема не меняются (AC-12).
|
||||
|
||||
## 1. Существующие таблицы — без изменений
|
||||
- `events`, `tasks`, `agent_runs`, `jobs` — структура не меняется.
|
||||
- В `tasks` НЕ вводится колонка статуса/окна наблюдения (намеренно — миграция на
|
||||
проде = риск, как обосновано в adr-0007; альтернатива отклонена в ADR-001 §Альтернативы).
|
||||
|
||||
## 2. Очередь `jobs` — переиспользование, без схемы
|
||||
- `post-deploy-monitor` — новый **job-kind** (значение в существующей колонке
|
||||
`agent`/`task_content`), НЕ новая колонка. Ставится через существующий
|
||||
`enqueue_job(..., available_at_delay_s=...)` (ORCH-1).
|
||||
- Счётчик тиков/деферов восстанавливается из jobs-очереди (как
|
||||
`_deploy_finalize_defer_count` считает по `task_content LIKE`), restart-safe.
|
||||
|
||||
## 3. Sentinel-состояние (файлы, не БД)
|
||||
State-dir `.post-deploy-state-<repo>/<work_item_id>/` на `settings.repos_dir`
|
||||
(по образцу `.deploy-state-*`):
|
||||
| Файл | Назначение |
|
||||
|------|------------|
|
||||
| `armed` | наблюдение заармлено (идемпотентность арма; калька `INITIATED`) |
|
||||
| `series` | JSON-список результатов опросов (счётчики health-fail / 5xx; restart-safe) |
|
||||
| `done` | наблюдение завершено (защита от повторной обработки) |
|
||||
|
||||
Все обращения — never-raise (по образцу `self_deploy.has_marker`/`write_marker`/
|
||||
`read_result`). Отсутствие/битость файла → консервативный фоллбэк, не исключение.
|
||||
|
||||
## 4. Артефакт `16-post-deploy-log.md` — файл репозитория, не БД
|
||||
Машиночитаемый YAML-frontmatter (`post_deploy_status`, `action_taken`, `window_s`,
|
||||
`checks_total`, `checks_failed`) пишется best-effort в `docs/work-items/<id>/`; в БД
|
||||
не реплицируется. Источник для петли уроков ORCH-8 (BR-10).
|
||||
|
||||
## 5. Очистка состояния
|
||||
По завершении окна / реакции `done`-маркер ставится; state-dir можно чистить
|
||||
best-effort (по образцу `self_deploy.clear_state`) — необязательно для корректности,
|
||||
но желательно для гигиены. Stale-`armed` без `done` после краха → виден в `/queue`
|
||||
как «активное наблюдение» и доигрывается восстановленным job'ом.
|
||||
20
docs/work-items/ORCH-021/10-tech-risks.md
Normal file
20
docs/work-items/ORCH-021/10-tech-risks.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 10 — Технические риски (ORCH-021)
|
||||
|
||||
| # | Риск | Вероятн. | Влияние | Митигация |
|
||||
|---|------|----------|---------|-----------|
|
||||
| R-1 | **Монитор self бежит внутри наблюдаемого прода.** Полностью wedged прод-контейнер → worker не выполнит тик → деградация не замечена, алерта нет. | Сред. | Высок. | Known MVP limitation (зафиксировано в ADR-001 §Последствия). Health в момент рестарта (хук) + reconciler ловят часть случаев. Внешний независимый watchdog — follow-up (вне рамок). |
|
||||
| R-2 | **Ложный авто-rollback** по сетевому глюку. | Низк. | Высок. | Пороги по N ПОСЛЕДОВАТЕЛЬНЫХ провалов + доля 5xx на окне (BR-3/AC-6), а не разовый провал. Self ВСЕГДА `ALERT_ONLY` (BR-5). `auto_rollback=False` по умолчанию. |
|
||||
| R-3 | **Авто-rollback прод-орка убивает инструмент всех проектов.** | Низк. | Критич. | Структурный инвариант: тик self НИКОГДА не откатывает/рестартит прод-контейнер (AC-8). Self → только alert + ручной approve. Откат self — только detached host-процесс вне тика. |
|
||||
| R-4 | **Нет prev-образа** при ROLLBACK → откат невозможен. | Сред. | Сред. | Хук возвращает exit 1 → `ROLLBACK_FAILED` + громкий алерт (AC-9), деградация не проглатывается тихо. |
|
||||
| R-5 | **Дубль/потеря наблюдения** при двойном webhook / рестарте. | Сред. | Сред. | Идемпотентность: sentinel `armed` (арм-гард) + `done` (защита от повторной обработки) + restart-safe jobs-очередь + `series` (AC-15). По образцу finalizer. |
|
||||
| R-6 | **Исключение в наблюдении роняет worker / конвейер других проектов.** | Низк. | Высок. | Контракт never-raise во всём `post_deploy.py` и `run_post_deploy_monitor` (AC-16), по образцу `self_deploy`/`staging_verdict`. |
|
||||
| R-7 | **Тик занимает single-worker** (`max_concurrency=1`) → задержка других задач. | Низк. | Низк. | Опрос короткий (~секунды), между тиками job не выполняется (defer через `available_at_delay_s`) — worker свободен, как у finalizer. Окно bounded (`window_s/interval_s`). |
|
||||
| R-8 | **Скрытое изменение контракта** (реестры/гейты/exit-коды/схема). | Низк. | Высок. | Инвариант: `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_deploy_status`/terminal-sync/merge-gate/exit-коды/схема БД НЕ меняются (AC-12). Существующие тесты deploy/staging/merge-gate должны остаться зелёными. |
|
||||
| R-9 | **5xx на `/queue`/`/status` из-за самого монитора** (рекурсивная нагрузка). | Низк. | Низк. | Интервал `post_deploy_interval_s` (30с) — низкая частота; опрос лёгкий GET. |
|
||||
| R-10 | **Артефакт `16-post-deploy-log.md` не пишется / невалиден** → петля уроков без данных. | Низк. | Низк. | Best-effort запись с валидным frontmatter (AC-13); отсутствие файла ничего не роняет. Парсинг — defensive. |
|
||||
|
||||
## Эскалация
|
||||
- Изменение помечено `arch:major-change` (новая под-компонента `src/post_deploy.py`
|
||||
+ новый reserved-agent job-kind `post-deploy-monitor`).
|
||||
- R-1 (gap наблюдения для wedged self-контейнера) — кандидат на отдельную задачу
|
||||
(внешний watchdog), вне рамок ORCH-021.
|
||||
99
docs/work-items/ORCH-021/12-review.md
Normal file
99
docs/work-items/ORCH-021/12-review.md
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-021
|
||||
verdict: APPROVED
|
||||
version: 2
|
||||
---
|
||||
|
||||
# Review ORCH-021 — Post-deploy мониторинг прода + реакция на деградацию
|
||||
|
||||
## Summary
|
||||
Реализация продлевает ответственность конвейера ЗА терминальный переход
|
||||
`deploy → done`, закрывая класс инцидентов «зелёный деплой, красный прод» (ET-8).
|
||||
Механизм — детерминированный reserved-agent job `post-deploy-monitor` (вариант B
|
||||
из ADR-001, точная калька `deploy-finalizer`): арм в `stage_engine.advance_stage`
|
||||
(блок `next_stage == "done"`), один тик = один job (перехват в
|
||||
`launcher.launch_job` ДО `_spawn` → `stage_engine.run_post_deploy_monitor`),
|
||||
чистая логика в новом leaf-модуле `src/post_deploy.py` (never-raise).
|
||||
|
||||
Проверены все четыре оси. Реализация соответствует ТЗ (`02-trz.md`), ADR-001 и
|
||||
глобальному adr-0010, удовлетворяет всем критериям приёмки AC-1…AC-18.
|
||||
Документация (golden-source) обновлена в том же PR. Регрессов нет.
|
||||
|
||||
## Соответствие ТЗ
|
||||
- §2.1 `src/post_deploy.py` (leaf, never-raise): `post_deploy_applies`,
|
||||
`probe_signals`, `classify`, `decide_action`, sentinel-state, артефакт,
|
||||
`build_rollback_command` — все на месте. ✅
|
||||
- §2.2 Оркестрация: арм в terminal-блоке + reserved-agent тик с
|
||||
само-перепостановкой через `available_at_delay_s`; restart-safe (sentinel
|
||||
`armed`/`series`/`done` + jobs-очередь). ✅
|
||||
- §2.3 Реакция: non-self+auto → хук `--rollback` (синхронно, целевой ≠ orch);
|
||||
self-hosting → ВСЕГДА `ALERT_ONLY`. ✅
|
||||
- §2.4 Конфигурация: все `post_deploy_*` в `src/config.py`, дефолты безопасны
|
||||
(kill-switch on, auto-rollback off), параметры отката переиспользуют
|
||||
`deploy_prod_*`. ✅
|
||||
- §2.5 Артефакт `16-post-deploy-log.md` с машиночитаемым frontmatter,
|
||||
best-effort. ✅
|
||||
- §2.6 Блок `post_deploy` в `GET /queue`. ✅
|
||||
- §2.7/§2.8/§3 Инварианты: `STAGE_TRANSITIONS`, `QG_CHECKS`,
|
||||
`check_deploy_status`, terminal-sync, merge-gate, exit-code-контракт хука,
|
||||
схема БД — не тронуты (подтверждено зелёным полным прогоном). ✅
|
||||
|
||||
## Соответствие ADR
|
||||
Реализация 1:1 повторяет ADR-001: механизм (reserved-agent, не стадия/не daemon),
|
||||
точки интеграции, пороги BR-3, политика реакции BR-5 (self never auto-rollback —
|
||||
структурный инвариант в `decide_action` + отсутствие вызова `run_rollback` на
|
||||
ALERT_ONLY). Нарушений глобальных ADR не выявлено.
|
||||
|
||||
## Качество кода
|
||||
- Контракт never-raise выдержан во всех публичных функциях и в каждой ветке
|
||||
`run_post_deploy_monitor`; launcher оборачивает тик в доп. guard (AC-16).
|
||||
- `classify` fail-safe → HEALTHY на мусорном входе (ложный DEGRADED опаснее).
|
||||
- Docstrings содержательные, со ссылками на AC/BR.
|
||||
- Условность раската по образцу ORCH-35/36/43/58 (флаг + CSV-репо).
|
||||
|
||||
## Тесты
|
||||
30 тестов ORCH-021 (`tests/test_post_deploy.py`,
|
||||
`tests/test_post_deploy_integration.py`) — содержательные, покрывают
|
||||
классификацию (AC-3..6), self-hosting safety (TC-19 явно проверяет, что хук
|
||||
`--rollback` НЕ вызывается для self — AC-8), idempotency двойного арма (AC-15),
|
||||
kill-switch/условность (AC-2/10/11), exit-code маппинг (AC-9), frontmatter
|
||||
артефакта (AC-13), never-raise (AC-16), `/queue` (AC-14). Полный прогон
|
||||
`pytest tests/` — **701 passed** (регрессов нет, AC-12).
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- нет
|
||||
|
||||
### P1 — Must fix
|
||||
- нет
|
||||
|
||||
### P2 — Should fix
|
||||
- нет
|
||||
|
||||
### P3 — Nice to have
|
||||
- [ ] `run_post_deploy_monitor`: в ветке `ALERT_ONLY` для **не-self** репо при
|
||||
`post_deploy_auto_rollback=false` текст алерта упоминает «авто-rollback для
|
||||
self-hosting запрещён (BR-5)», что для не-self случая формулировка не совсем
|
||||
точна (косметика сообщения; на поведение не влияет).
|
||||
- [ ] `write_post_deploy_log` коммитит/пушит артефакт в ветку задачи, которая к
|
||||
моменту наблюдения уже слита/может быть удалена — артефакт может не попасть в
|
||||
`main`. Контракт best-effort соблюдён (never-raise, ничего не роняет); как
|
||||
улучшение наблюдаемости — рассмотреть запись лог-артефакта отдельным путём.
|
||||
|
||||
## Документация
|
||||
Обновлено в том же PR (golden-source, AC-18 — PASS):
|
||||
- `CLAUDE.md` — `16-post-deploy-log.md` добавлен в перечень артефактов;
|
||||
- `docs/architecture/README.md` — раздел «Post-deploy наблюдение прода» + блок
|
||||
`post_deploy` в таблице API `/queue`;
|
||||
- `docs/architecture/adr/adr-0010-post-deploy-monitor.md` — новый сквозной ADR;
|
||||
- `docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md` — детальный ADR;
|
||||
- `CHANGELOG.md` — запись в `Added` (+ fix Dockerfile `COPY data/`);
|
||||
- `README.md` / `.env.example` — все `ORCH_POST_DEPLOY_*` env задокументированы.
|
||||
|
||||
Изменение `src/` сопровождено обновлением документации — правило CLAUDE.md №2/№6
|
||||
выполнено.
|
||||
|
||||
## Вердикт
|
||||
Только P3 (nice-to-have) findings, блокеров и must-fix нет → **APPROVED**.
|
||||
82
docs/work-items/ORCH-021/13-test-report.md
Normal file
82
docs/work-items/ORCH-021/13-test-report.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-021
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-021
|
||||
|
||||
Post-deploy наблюдение прода + реакция на деградацию (reserved-agent job
|
||||
`post-deploy-monitor`, leaf-модуль `src/post_deploy.py`).
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3 (asyncio mode=AUTO, anyio 4.13.0)
|
||||
- Ветка: feature/ORCH-021-post-deploy-rollback
|
||||
- Дата: 2026-06-07
|
||||
|
||||
## Прогон
|
||||
- `pytest tests/ -v --tb=short` → **701 passed, 1 warning** (Pydantic V2 deprecation, не относится к задаче).
|
||||
- Целевые модули `tests/test_post_deploy.py` + `tests/test_post_deploy_integration.py` → **30 passed**.
|
||||
|
||||
## Smoke-test (read-only, прод 8500)
|
||||
`curl` в окружении недоступен — опрос через `python urllib` (read-only, прод-контейнер не трогается).
|
||||
|
||||
| Эндпоинт | Результат |
|
||||
|----------|-----------|
|
||||
| `GET /health` | 200 `{"status":"ok","service":"orchestrator"}` |
|
||||
| `GET /status` | 200, активная задача ORCH-021 на стадии `testing` |
|
||||
| `GET /queue` | 200, counts/resilience/reconcile присутствуют |
|
||||
|
||||
> Примечание: блок `post_deploy` в **живом** `/queue` отсутствует — это ожидаемо: прод
|
||||
> сейчас работает на коде ДО ORCH-021 (задача ещё не задеплоена, стадия testing).
|
||||
> Наличие блока (AC-14) проверяется интеграционным тестом TC-20 против кода ветки → PASS.
|
||||
> Smoke-проверка подтверждает живость окружения, не версию ветки.
|
||||
|
||||
## Результаты по тест-плану (04-test-plan.yaml)
|
||||
|
||||
| TC ID | Описание | Покрывает AC | Тест-функция | Результат |
|
||||
|-------|----------|--------------|--------------|-----------|
|
||||
| TC-01 | HEALTHY: серия без провалов < порога | AC-3 | test_tc01_healthy_no_failures | PASS |
|
||||
| TC-02 | DEGRADED: N посл. провалов health == threshold | AC-4 | test_tc02_degraded_consecutive_health_failures | PASS |
|
||||
| TC-03 | DEGRADED по 5xx при health=200 | AC-5 | test_tc03_degraded_by_5xx_ratio_even_when_health_200 | PASS |
|
||||
| TC-04 | Нет ложного срабатывания: одиночный глюк + восстановление | AC-6 | test_tc04_no_false_trip_single_glitch_then_recovery | PASS |
|
||||
| TC-05 | Пороги из Settings меняют вердикт на тех же данных | AC-11 | test_tc05_thresholds_change_verdict_on_same_data, test_classify_uses_settings_thresholds | PASS |
|
||||
| TC-06 | не-self + auto_rollback=True + DEGRADED → ROLLBACK | AC-7 | test_tc06_nonself_auto_rollback_degraded_rolls_back | PASS |
|
||||
| TC-07 | self-hosting + DEGRADED → ALERT_ONLY (никогда не авто-rollback) | AC-8 | test_tc07_self_hosting_degraded_never_rolls_back | PASS |
|
||||
| TC-08 | HEALTHY → NONE для любого репо | AC-3 | test_tc08_healthy_means_none_for_any_repo, test_nonself_default_policy_alert_only | PASS |
|
||||
| TC-09 | post_deploy_applies: пусто → только orchestrator | AC-2 | test_tc09_applies_empty_repos_only_self_hosting, test_tc09_applies_explicit_repos_csv | PASS |
|
||||
| TC-10 | kill-switch: monitor_enabled=False → applies()=False для всех | AC-10 | test_tc10_kill_switch_disables_for_everyone | PASS |
|
||||
| TC-11 | Откат exit 0 → ROLLBACK_OK | AC-7 | test_tc11_rollback_exit0_is_ok | PASS |
|
||||
| TC-12 | Откат exit 1/2 → ROLLBACK_FAILED + эскалация | AC-9 | test_tc12_rollback_exit_nonzero_is_failed | PASS |
|
||||
| TC-13 | 16-post-deploy-log.md: валидный YAML-frontmatter | AC-13 | test_tc13_log_frontmatter_parses | PASS |
|
||||
| TC-14 | Опрос при сетевой ошибке → консервативный, не raise | AC-16 | test_tc14_probe_network_error_is_conservative_not_raise, test_tc14_classify_junk_input_swallowed | PASS |
|
||||
| TC-15 | Ошибка записи артефакта → False, не raise | AC-16, AC-13 | test_tc15_write_log_no_worktree_returns_false | PASS |
|
||||
| TC-16 | advance_stage deploy→done армит наблюдение (self), не армит (non-self) | AC-1, AC-2 | test_tc16_arm_for_self_hosting, test_tc16_no_arm_for_nonself, test_tc16_no_arm_when_kill_switch_off | PASS |
|
||||
| TC-17 | Идемпотентность: повторный арм не задваивает | AC-15 | test_tc17_double_arm_is_noop | PASS |
|
||||
| TC-18 | Полный цикл DEGRADED → не-self откат + лог + уведомление | AC-7, AC-13, AC-17 | test_tc18_degraded_nonself_rolls_back | PASS |
|
||||
| TC-19 | Self-hosting DEGRADED → НЕ рестарт/откат, алерт+approve | AC-8, AC-17 | test_tc19_degraded_self_hosting_alert_only | PASS |
|
||||
| TC-20 | GET /queue содержит блок post_deploy | AC-14 | test_tc20_queue_block_present | PASS |
|
||||
| TC-21 | Регресс: deploy/staging/merge-gate/reconciler зелёные; STAGE_TRANSITIONS/QG_CHECKS не изменены | AC-12 | tests/test_stages.py (+ полный прогон 701) | PASS |
|
||||
|
||||
Доп. тесты ветки (не из плана, подтверждают контракты): `test_series_append_and_read_roundtrip`,
|
||||
`test_mark_done_idempotency_marker`, `test_healthy_tick_requeues_without_finishing`,
|
||||
`test_finished_window_tick_is_noop` — все PASS.
|
||||
|
||||
## Покрытие критериев приёмки
|
||||
AC-1…AC-18 — все покрыты прошедшими тестами (см. таблицу). AC-12 (реестры/схема БД
|
||||
не изменены) дополнительно подтверждён зелёным полным регрессом 701 теста, включая
|
||||
deploy/staging/merge-gate/reconciler. AC-18 (документация) — вне scope прогона тестов,
|
||||
подтверждён ревью (12-review.md, verdict APPROVED).
|
||||
|
||||
## Вывод pytest (хвост)
|
||||
```
|
||||
======================= 701 passed, 1 warning in 12.71s ========================
|
||||
```
|
||||
```
|
||||
======================== 30 passed, 1 warning in 0.58s =========================
|
||||
```
|
||||
|
||||
## Итог
|
||||
**PASS.** Все 21 тест-кейс плана зелёные, полный регресс (701) зелёный, smoke прод-эндпоинтов
|
||||
OK (окружение живо). Существующие контракты не сломаны. Задача готова к стадии deploy-staging.
|
||||
42
docs/work-items/ORCH-021/15-staging-log.md
Normal file
42
docs/work-items/ORCH-021/15-staging-log.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-07T14:37:33Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed. Verdict: **SUCCESS** (exit 0).
|
||||
|
||||
Run canonically inside the `orchestrator-staging` container (ORCH-048, ADR-001)
|
||||
via the Docker Engine API over the mounted socket (`docker` CLI is not installed
|
||||
in the prod-agent container; `network_mode: host` + group `999` allow direct
|
||||
socket access):
|
||||
|
||||
```
|
||||
python3 /repos/orchestrator/scripts/staging_check.py \
|
||||
--base-url http://localhost:8501 --mode stub
|
||||
```
|
||||
|
||||
## Result
|
||||
|
||||
```
|
||||
RESULT: 8/10 checks PASS
|
||||
REAL failed : none
|
||||
SANDBOX_INFRA failed: ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue']
|
||||
tolerance: staging_infra_tolerance_enabled=True
|
||||
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
|
||||
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
|
||||
```
|
||||
|
||||
- **Block A (SMOKE):** A1 `/health` 200 ok, A2 `/queue` 200, A3 `ORCH_STAGING=true` — all PASS.
|
||||
- **Block B (ACCESS):** B4 Plane sandbox, B5 Gitea `orchestrator-sandbox` (push=true),
|
||||
B6 registry isolation (sandbox present, prod ET/ORCH absent) — all PASS.
|
||||
- **Block C (E2E, stub):** C7 create issue in SANDBOX, C8 trigger pipeline via
|
||||
`/webhook/plane` — PASS. C9a/C9b FAILED but are sandbox-infra checks (bot accounts
|
||||
not members of the SANDBOX Plane project) — **waived** per ORCH-061; not a pipeline
|
||||
regression. Cleanup deleted the test Plane issue (HTTP 204).
|
||||
|
||||
All REAL pipeline checks are green; the only failures are the two known
|
||||
sandbox-infra checks, which the verdict tolerates (`staging_infra_tolerance_enabled=true`).
|
||||
The script exited 0 → advance.
|
||||
30
docs/work-items/ORCH-022/15-staging-log.md
Normal file
30
docs/work-items/ORCH-022/15-staging-log.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-07T18:02:27+00:00
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed via canonical run (ORCH-048, ADR-001):
|
||||
|
||||
```
|
||||
docker exec orchestrator-staging \
|
||||
python3 /repos/orchestrator/scripts/staging_check.py \
|
||||
--base-url http://localhost:8501 --mode stub
|
||||
```
|
||||
|
||||
**Result: 8/10 checks PASS — exit code 0 (advance).**
|
||||
|
||||
All REAL (pipeline) checks green: A1, A2, A3 (SMOKE), B4, B5, B6 (ACCESS), C7, C8 (E2E).
|
||||
|
||||
Two sandbox-infra-only checks failed and were waived per ORCH-061
|
||||
(`staging_infra_tolerance_enabled=True`) — these depend on SANDBOX bot accounts
|
||||
being members of the SANDBOX Plane project, not on the pipeline:
|
||||
|
||||
```
|
||||
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
|
||||
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
|
||||
```
|
||||
|
||||
Cleanup ran (Plane SANDBOX test issue deleted, HTTP 204). Exit code 0 → `staging_status: SUCCESS`.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-06T21:06:37Z
|
||||
timestamp: 2026-06-06T21:47:48Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
|
||||
7
docs/work-items/ORCH-058/00-business-request.md
Normal file
7
docs/work-items/ORCH-058/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: Self-deploy: retag берёт устаревший staging-образ (риск тихого регресса)
|
||||
|
||||
Work Item ID: ORCH-058
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
87
docs/work-items/ORCH-058/01-brd.md
Normal file
87
docs/work-items/ORCH-058/01-brd.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# BRD — ORCH-058: Self-deploy retag берёт устаревший staging-образ (риск тихого регресса)
|
||||
|
||||
Work Item ID: ORCH-058
|
||||
Тип: bug / техдолг инфраструктуры self-deploy
|
||||
Источник: `docs/history/LESSONS_ORCH-036-selfdeploy.md` п.4 (самый опасный из 4 багов bootstrap ORCH-36)
|
||||
|
||||
## 1. Контекст
|
||||
|
||||
ORCH-36 сделал стадию `deploy` исполняемой для self-hosting репозитория `orchestrator`:
|
||||
- Phase B (`src/self_deploy.py::build_deploy_command`) запускает детачед host-хук
|
||||
`scripts/orchestrator-deploy-hook.sh` с параметром `SOURCE_IMAGE=orchestrator-orchestrator-staging`.
|
||||
- Хук (шаг **2b**, BUILD-ONCE, ORCH-36 BR-6) делает `docker tag $SOURCE_IMAGE → $TARGET_IMAGE`
|
||||
**без `docker build`** — «прод получает ровно тот артефакт, что прошёл staging».
|
||||
|
||||
Дизайн-предпосылка BUILD-ONCE: **staging-образ свеж и провалидирован**. На практике этой
|
||||
гарантии НЕТ.
|
||||
|
||||
## 2. Проблема (корень)
|
||||
|
||||
Конвейер **нигде не пересобирает** образ `orchestrator-orchestrator-staging` из текущего
|
||||
кода (HEAD `main` / провалидированной ветки):
|
||||
- Стадия `deploy-staging` запускает только `scripts/staging_check.py` (e2e-проверка)
|
||||
против **уже работающего** контейнера `orchestrator-staging` (8501) — что бы в нём ни
|
||||
крутилось. Сборка staging-образа — ручная операция (STAGING.md / ORCH-34), вне конвейера.
|
||||
- Между «образ собран» и «retag в прод» нет провенанс-связи с провалидированным коммитом.
|
||||
|
||||
Следствие (инцидент ORCH-36): staging-образ не пересобрали из нового `main` →
|
||||
`staging_check` прошёл против СТАРОГО кода → BUILD-ONCE retag промоутнул СТАРЫЙ образ в прод.
|
||||
Деплой «зелёный» (`result=0`, health ok), но прод молча откатился на код 2-дневной давности:
|
||||
пропал `deploy-finalizer` → задача не закрылась → бесконечная петля Phase B.
|
||||
|
||||
## 3. Почему это критично
|
||||
|
||||
> Это **самый опасный** из четырёх багов self-deploy: он **не падает**, а **тихо откатывает
|
||||
> прод**. Зелёный гейт = ложный позитив. Орк обслуживает все проекты (enduro-trails) из одного
|
||||
> прод-инстанса → тихий регресс инструмента = групповой инцидент для всех проектов.
|
||||
|
||||
Текущая защита (staging-гейт, merge-gate, health-check хука) НЕ ловит этот класс: все они
|
||||
зелёные, потому что проверяют не тот артефакт, что уезжает в прод.
|
||||
|
||||
## 4. Бизнес-цель
|
||||
|
||||
Гарантировать инвариант: **в прод никогда не промоутится образ, не собранный из
|
||||
провалидированного для данной задачи коммита; при невозможности это доказать — деплой
|
||||
fail-fast (вердикт FAILED → откат на development), а не «тихо зелёный»**.
|
||||
|
||||
## 5. Объём (scope)
|
||||
|
||||
В объёме:
|
||||
- Привязка артефакта (staging-образ → прод-retag) к провалидированному коммиту.
|
||||
- Fail-fast при рассинхроне образа и кода (никаких тихих промоутов устаревшего).
|
||||
- Условность как ORCH-35/36/43: реально только для `orchestrator`; прочие репо — no-op /
|
||||
прежнее поведение.
|
||||
- Контракт never-raise и fail-closed (на сомнении — не деплоить).
|
||||
|
||||
Вне объёма:
|
||||
- Полный авто-approve прод-деплоя (ORCH-54).
|
||||
- Изменение exit-code-контракта хука (0/1/2) и реестров `STAGE_TRANSITIONS` / `QG_CHECKS` как
|
||||
набора стадий.
|
||||
- Миграции схемы БД.
|
||||
- Деплой/рестарт **прод**-контейнера `orchestrator` (8500) в рамках задачи.
|
||||
|
||||
## 6. Бизнес-требования (BR)
|
||||
|
||||
- **BR-1.** Образ, который BUILD-ONCE retag промоутит в прод, ДОЛЖЕН соответствовать коду,
|
||||
провалидированному стадией `deploy-staging` для данной задачи (тот же git-коммит).
|
||||
- **BR-2.** Если соответствие НЕ доказуемо (staging-образ собран не из провалидированного
|
||||
коммита, либо провенанс невозможно прочесть) — деплой ОБЯЗАН fail-fast: вердикт `FAILED`,
|
||||
штатный откат на `development` (контракт БАГ-8), без рестарта прода.
|
||||
- **BR-3.** `staging_check.py` (e2e-валидация) ДОЛЖЕН прогоняться против артефакта,
|
||||
соответствующего тому же провалидированному коммиту, что уедет в прод (нельзя валидировать
|
||||
один образ, а катить другой).
|
||||
- **BR-4.** Поведение условно: реально для `orchestrator`; для прочих репозиториев — no-op /
|
||||
без регрессий прежнего синхронного деплоя.
|
||||
- **BR-5.** Выбранное решение НЕ должно приводить к вечной блокировке деплоя (если механизм
|
||||
свежести отсутствует — нужен путь, который доводит до зелёного, а не fail-fast'ит навсегда).
|
||||
- **BR-6.** Контракт never-raise: сбой проверки свежести/провенанса не должен валить
|
||||
stage_engine; на любом сомнении — fail-closed (трактуем как несоответствие).
|
||||
- **BR-7.** Документация-голден-сорс: INFRA / DEPLOY_HOOK / STAGING / architecture README +
|
||||
CHANGELOG обновляются в том же PR; решение оформляется ADR.
|
||||
|
||||
## 7. Связанные материалы
|
||||
|
||||
- `docs/history/LESSONS_ORCH-036-selfdeploy.md` (п.4 — корень)
|
||||
- `docs/architecture/adr/adr-0007-executable-self-deploy.md`, `docs/work-items/ORCH-036/06-adr/ADR-001-executable-self-deploy.md`
|
||||
- `src/self_deploy.py`, `scripts/orchestrator-deploy-hook.sh`, `src/config.py`
|
||||
- `docs/operations/STAGING.md`, `docs/operations/DEPLOY_HOOK.md`, `docs/operations/INFRA.md`
|
||||
126
docs/work-items/ORCH-058/02-trz.md
Normal file
126
docs/work-items/ORCH-058/02-trz.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# ТЗ — ORCH-058: провенанс staging-образа перед BUILD-ONCE retag в прод
|
||||
|
||||
Work Item ID: ORCH-058
|
||||
|
||||
> Примечание: ТЗ фиксирует ТРЕБУЕМЫЕ изменения и точки в коде. **Выбор стратегии**
|
||||
> (пересборка из HEAD `main` ПЕРЕД валидацией vs. fail-fast по провенансу образа, либо их
|
||||
> комбинация) — решение **архитектора** (ADR в `06-adr/`). Ниже перечислены точки
|
||||
> касания для обеих стратегий; архитектор выбирает и при необходимости сужает.
|
||||
|
||||
## 1. Инвариант, который нужно обеспечить
|
||||
|
||||
`INV-FRESH`: образ, передаваемый хуку как `SOURCE_IMAGE` для BUILD-ONCE retag в прод,
|
||||
собран из ТОГО ЖЕ git-коммита, что прошёл `deploy-staging` для этой задачи. Если это
|
||||
недоказуемо — деплой fail-fast (`deploy_status: FAILED` → откат на `development`, БАГ-8),
|
||||
прод не трогается.
|
||||
|
||||
Якорь «провалидированного коммита» (architect фиксирует точно в ADR): SHA HEAD ветки задачи
|
||||
после merge-gate rebase на `origin/main` (то, что валидировал `deploy-staging` + merge-gate).
|
||||
|
||||
## 2. Текущее поведение (что чинить)
|
||||
|
||||
| Место | Сейчас | Проблема |
|
||||
|---|---|---|
|
||||
| `scripts/orchestrator-deploy-hook.sh` шаг 2b | `docker tag $SOURCE_IMAGE → $TARGET_IMAGE` без проверки происхождения образа | промоутит любой образ под именем `orchestrator-orchestrator-staging`, даже устаревший |
|
||||
| Стадия `deploy-staging` (`.openclaw/agents/deployer.md` + `staging_check.py`) | гоняет e2e против уже запущенного 8501, не пересобирая образ | валидирует не тот артефакт, что уедет в прод |
|
||||
| `src/self_deploy.py::build_deploy_command` | передаёт `SOURCE_IMAGE`, `TARGET_*`, `COMPOSE_PROFILE`, `PREV_IMAGE_FILE`; провенанс/SHA не передаёт | хук не знает, какой коммит ожидать |
|
||||
| `Dockerfile` | без OCI-лейбла `revision`/git-SHA | у образа нет машиночитаемого происхождения для проверки |
|
||||
|
||||
## 3. Задействованные модули `src/` и файлы
|
||||
|
||||
- `src/self_deploy.py` — основной (provenance-helpers + проброс ожидаемого SHA в команду хука).
|
||||
- `src/config.py` — новые настройки (`ORCH_`-префикс обязателен, урок ORCH-36 п.2).
|
||||
- `scripts/orchestrator-deploy-hook.sh` — fail-fast по провенансу и/или пересборка перед retag.
|
||||
- `Dockerfile` — лейбл происхождения образа (для стратегии «провенанс по labels/sha»).
|
||||
- `src/qg/checks.py` — опц. новый детерминированный под-чек свежести (если стратегия «гейт»).
|
||||
- `src/stage_engine.py` — опц. точка вызова под-чека на ребре `deploy-staging → deploy`
|
||||
(рядом с merge-gate, строки ~262–288). **Реестр `STAGE_TRANSITIONS` не меняется.**
|
||||
- `.openclaw/agents/deployer.md` — шаги стадии `deploy-staging` (если выбран rebuild-перед-валидацией).
|
||||
- `docker-compose.yml` — опц. build-args/labels для staging-сервиса (если стратегия rebuild).
|
||||
|
||||
## 4. Требуемые изменения — стратегия A (пересборка из HEAD main перед валидацией)
|
||||
|
||||
A1. Перед прогоном `staging_check.py` стадия `deploy-staging` для `orchestrator` пересобирает
|
||||
образ `orchestrator-orchestrator-staging` из провалидированного коммита (worktree ветки
|
||||
после merge-gate rebase) и пересоздаёт контейнер 8501 на свежем образе.
|
||||
A2. `staging_check.py` гоняется против свежего контейнера; на `SUCCESS` ровно ЭТОТ образ
|
||||
становится `SOURCE_IMAGE` для прод-retag (loop closed).
|
||||
A3. Детерминированно (без LLM в критическом пути): сборку/recreate выполняет код стадии или
|
||||
host-хук в staging-режиме, не агент-деплойер «руками».
|
||||
A4. Безопасность: операция трогает ТОЛЬКО staging (8501), НИКОГДА прод (8500).
|
||||
|
||||
## 5. Требуемые изменения — стратегия B (fail-fast по провенансу образа)
|
||||
|
||||
B1. `Dockerfile`: добавить лейбл происхождения, напр.
|
||||
`LABEL org.opencontainers.image.revision=$GIT_SHA` через `ARG GIT_SHA` (build-arg).
|
||||
B2. Сборка staging-образа (ручная или из стратегии A) проставляет `GIT_SHA` = коммит сборки.
|
||||
B3. `src/self_deploy.py::build_deploy_command`: вычислить ожидаемый SHA провалидированного
|
||||
коммита и пробросить в команду хука новым env (напр. `EXPECTED_REVISION=<sha>`).
|
||||
Новый pure-helper, напр. `expected_revision(repo, branch) -> str` (never-raise).
|
||||
B4. `scripts/orchestrator-deploy-hook.sh` шаг 2b: ПЕРЕД `docker tag` прочитать лейбл
|
||||
`$SOURCE_IMAGE` (`docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}'`)
|
||||
и сравнить с `$EXPECTED_REVISION`. Несовпадение / пустой лейбл / пустой ожидаемый SHA →
|
||||
`log` + `exit 1` (fail-fast). Поведение обратносовместимо: при незаданном
|
||||
`EXPECTED_REVISION` — текущее поведение (без проверки), чтобы не сломать не-self репо.
|
||||
B5. exit 1 хука уже маппится `map_exit_code_to_status → FAILED` (контракт не меняется),
|
||||
Phase C пишет `14-deploy-log.md` `deploy_status: FAILED` → откат на `development` (БАГ-8).
|
||||
|
||||
## 6. Требуемые изменения — опц. под-гейт (если архитектор выберет gate-side для B)
|
||||
|
||||
- Новый детерминированный (без LLM) под-чек, напр. `check_staging_image_fresh`, по образцу
|
||||
`check_branch_mergeable` (ORCH-043): pure verdict-logic + условность (`self_deploy_applies`
|
||||
/ `is_self_hosting_repo`), never-raise, для прочих репо → `(True, "N/A")`.
|
||||
- Вызов на ребре `deploy-staging → deploy` ПЕРЕД Phase A (рядом с merge-gate, `stage_engine`
|
||||
~268–288). FAIL → откат на `development` (как merge-gate). Реестр стадий неизменен —
|
||||
это под-гейт ребра, не новая стадия.
|
||||
- Если выбран чисто хуковый fail-fast (раздел 5) — под-гейт не нужен.
|
||||
|
||||
## 7. Изменения API
|
||||
|
||||
Нет. Эндпоинты (`/health`, `/status`, `/queue`, `/webhook/*`) не меняются. Опц.: в снимок
|
||||
`GET /queue` можно добавить диагностическое поле о свежести образа — НЕ обязательно.
|
||||
|
||||
## 8. Изменения схемы БД
|
||||
|
||||
Нет. Состояние deploy — sentinel-файлы (`.deploy-state-<repo>/<wi>/`, ORCH-36). Миграции
|
||||
запрещены (как ORCH-36/43/53).
|
||||
|
||||
## 9. Конфигурация (`src/config.py`, ВСЕ с префиксом `ORCH_`)
|
||||
|
||||
Кандидаты (architect финализирует имена и дефолты):
|
||||
- `image_freshness_enabled: bool = True` — kill-switch проверки (поэтапный раскат).
|
||||
- `image_freshness_repos: str = ""` — CSV; пусто → только self-hosting (как `self_deploy_repos`).
|
||||
- (для стратегии B) проброс `EXPECTED_REVISION` строится в `build_deploy_command`, отдельной
|
||||
настройки может не требоваться.
|
||||
- (для стратегии A) при необходимости — имя/тег staging-образа уже есть
|
||||
(`deploy_prod_source_image`).
|
||||
|
||||
Урок ORCH-36 п.2: любая настройка, читаемая pydantic Settings, ОБЯЗАНА иметь префикс `ORCH_`.
|
||||
|
||||
## 10. Новые QG checks (если применимо)
|
||||
|
||||
- Опц. `check_staging_image_fresh` (см. §6) — добавить в реестр `QG_CHECKS` и в
|
||||
snapshot-тест реестра (`tests/test_qg_registry_snapshot.py`). Только если выбран gate-side.
|
||||
|
||||
## 11. Артефакты pipeline (создать/обновить В ТОМ ЖЕ PR)
|
||||
|
||||
- `06-adr/ADR-001-<slug>.md` — выбор стратегии (A / B / A+B), якорь «провалидированного
|
||||
коммита», точки fail-fast, условность, never-raise, отсутствие deadlock (BR-5).
|
||||
- `docs/operations/DEPLOY_HOOK.md` — описание провенанс-проверки / пересборки и новых env.
|
||||
- `docs/operations/STAGING.md` — как и когда пересобирается staging-образ в конвейере.
|
||||
- `docs/operations/INFRA.md` — обновить топологию/риск self-deploy (закрыт п.4 каскада).
|
||||
- `docs/architecture/README.md` — секция ORCH-36/58 (свежесть артефакта в BUILD-ONCE).
|
||||
- `CHANGELOG.md` — запись ORCH-058.
|
||||
- При выборе стратегии A: bootstrap-чеклист (урок ORCH-36 «сквозной»: реальный staging-прогон
|
||||
до мержа).
|
||||
|
||||
## 12. Инварианты / ограничения (self-hosting safety)
|
||||
|
||||
- Никогда не рестартовать/ронять прод 8500 в рамках задачи (CLAUDE.md). Любая сборка/recreate —
|
||||
только staging 8501.
|
||||
- Никогда не пушить/форс-пушить `main` (как merge-gate).
|
||||
- Контракты НЕ меняются: exit-code хука (0/1/2), `map_exit_code_to_status`,
|
||||
`check_deploy_status`/`_parse_deploy_status`, БАГ-8 rollback, terminal-sync, merge-gate.
|
||||
- Fail-closed: на любом сомнении (нет лейбла, нет ожидаемого SHA, ошибка inspect) —
|
||||
трактовать как несоответствие → FAILED, никогда не промоутить «на авось».
|
||||
- never-raise: helpers и под-чек не должны пробрасывать исключение в stage_engine.
|
||||
71
docs/work-items/ORCH-058/03-acceptance-criteria.md
Normal file
71
docs/work-items/ORCH-058/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Критерии приёмки — ORCH-058
|
||||
|
||||
Work Item ID: ORCH-058
|
||||
|
||||
Критерии сформулированы вокруг инварианта `INV-FRESH` и **не зависят** от выбранной
|
||||
архитектором стратегии (A — пересборка, B — fail-fast по провенансу, A+B). Каждый — с
|
||||
чётким условием PASS/FAIL.
|
||||
|
||||
## AC-1 — Соответствие артефакта коду (центральный инвариант)
|
||||
- PASS: образ, который BUILD-ONCE retag промоутит в прод (`SOURCE_IMAGE`), доказуемо собран
|
||||
из коммита, провалидированного стадией `deploy-staging` для этой задачи.
|
||||
- FAIL: в прод может уехать образ, собранный не из провалидированного коммита.
|
||||
|
||||
## AC-2 — Fail-fast при рассинхроне (никаких тихих зелёных)
|
||||
- PASS: если staging-образ собран НЕ из провалидированного коммита (или провенанс нечитаем),
|
||||
деплой завершается `deploy_status: FAILED` и откатом на `development` (БАГ-8); прод НЕ
|
||||
рестартуется на устаревший образ.
|
||||
- FAIL: при рассинхроне деплой завершается `SUCCESS` / «зелёным», прод тихо откатывается.
|
||||
|
||||
## AC-3 — Fail-closed на сомнении
|
||||
- PASS: при отсутствии лейбла происхождения, пустом ожидаемом SHA, ошибке `docker image
|
||||
inspect` или любой неоднозначности — трактуется как несоответствие → FAILED (никогда не
|
||||
промоутится «на авось»).
|
||||
- FAIL: сомнительный/непроверяемый случай трактуется как «свежий» и промоутится.
|
||||
|
||||
## AC-4 — Валидация и промоут — один и тот же артефакт
|
||||
- PASS: `staging_check.py` прогоняется против образа/контейнера, соответствующего тому же
|
||||
провалидированному коммиту, который затем уезжает в прод.
|
||||
- FAIL: валидируется один образ, а в прод retag'ается другой.
|
||||
|
||||
## AC-5 — Условность (self-hosting only)
|
||||
- PASS: проверка/пересборка реальна только для `orchestrator` (и репо из `image_freshness_repos`,
|
||||
если задан); для прочих репо — no-op, синхронный деплой не-self репо без регрессий.
|
||||
- FAIL: логика срабатывает для не-self репозиториев или ломает их деплой.
|
||||
|
||||
## AC-6 — Никакого deadlock деплоя (BR-5)
|
||||
- PASS: при штатном прогоне (staging-образ корректно отражает провалидированный коммит)
|
||||
деплой доходит до `SUCCESS` и `deploy → done`; механизм свежести не блокирует валидный
|
||||
деплой навсегда.
|
||||
- FAIL: валидный деплой вечно fail-fast'ится / задача зависает на `deploy`.
|
||||
|
||||
## AC-7 — Контракты не изменены
|
||||
- PASS: `STAGE_TRANSITIONS` (набор стадий), exit-code-контракт хука (0/1/2),
|
||||
`map_exit_code_to_status`, `check_deploy_status`/`_parse_deploy_status`, БАГ-8 rollback,
|
||||
terminal-sync, merge-gate — без изменений; схема БД без миграций.
|
||||
- FAIL: затронут любой из перечисленных контрактов или добавлена миграция БД.
|
||||
|
||||
## AC-8 — never-raise
|
||||
- PASS: сбой проверки свежести/провенанса (битый образ, ssh/docker error, отсутствующий
|
||||
worktree) не пробрасывает исключение в `stage_engine`; возвращается безопасный вердикт.
|
||||
- FAIL: исключение из новой логики всплывает и валит обработку стадии.
|
||||
|
||||
## AC-9 — Self-hosting safety
|
||||
- PASS: новая логика НЕ рестартует/не роняет прод-контейнер `orchestrator` (8500) и не
|
||||
пушит/форс-пушит `main`; любые сборки/recreate — только staging (8501).
|
||||
- FAIL: нарушено любое из ограничений выше.
|
||||
|
||||
## AC-10 — Конфигурация и kill-switch
|
||||
- PASS: новые настройки имеют префикс `ORCH_`; есть kill-switch (напр. `image_freshness_enabled`)
|
||||
для поэтапного раската; при выключенном флаге — прежнее поведение.
|
||||
- FAIL: настройка без `ORCH_`-префикса (не читается pydantic) или нет способа отключить.
|
||||
|
||||
## AC-11 — Документация (golden source)
|
||||
- PASS: в том же PR обновлены DEPLOY_HOOK.md, STAGING.md, INFRA.md, architecture/README.md,
|
||||
CHANGELOG.md и заведён ADR `06-adr/ADR-001-*`.
|
||||
- FAIL: функционал изменён, документация/ADR не обновлены (→ reviewer REQUEST_CHANGES).
|
||||
|
||||
## AC-12 — Тесты зелёные
|
||||
- PASS: `pytest tests/ -q` зелёный, включая новые тесты из `04-test-plan.yaml` и
|
||||
snapshot-тест реестра QG (если добавлен под-чек).
|
||||
- FAIL: любой тест из плана красный или регрессия существующих.
|
||||
124
docs/work-items/ORCH-058/04-test-plan.yaml
Normal file
124
docs/work-items/ORCH-058/04-test-plan.yaml
Normal file
@@ -0,0 +1,124 @@
|
||||
work_item: ORCH-058
|
||||
description: >
|
||||
Провенанс staging-образа перед BUILD-ONCE retag в прод. Тесты покрывают инвариант
|
||||
INV-FRESH: соответствие промоутируемого образа провалидированному коммиту, fail-fast
|
||||
и fail-closed при рассинхроне, условность self-hosting, never-raise, неизменность
|
||||
контрактов. Часть кейсов помечена strategy-зависимыми (A=пересборка, B=fail-fast по
|
||||
провенансу) — финальный набор подтверждает архитектор в ADR; пишутся тесты для
|
||||
выбранной стратегии.
|
||||
tests:
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: >
|
||||
Pure provenance-verdict: SHA образа == ожидаемый SHA -> свежий (PASS).
|
||||
Совпадающие revision дают вердикт "соответствует".
|
||||
module: tests/test_image_freshness.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: >
|
||||
Pure provenance-verdict: SHA образа != ожидаемый SHA -> НЕ свежий ->
|
||||
вердикт несоответствия (вход для fail-fast).
|
||||
module: tests/test_image_freshness.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: >
|
||||
Fail-closed: пустой/отсутствующий лейбл образа ИЛИ пустой ожидаемый SHA ->
|
||||
трактуется как несоответствие (никогда не "свежий по умолчанию").
|
||||
module: tests/test_image_freshness.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: >
|
||||
never-raise: provenance-helper при docker/ssh/inspect ошибке или отсутствующем
|
||||
worktree возвращает безопасный вердикт (несоответствие), не пробрасывает исключение.
|
||||
module: tests/test_image_freshness.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: >
|
||||
Условность: для не-self репозитория проверка свежести = no-op (True/"N/A");
|
||||
для orchestrator (или репо из image_freshness_repos) — реальна.
|
||||
module: tests/test_image_freshness.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: >
|
||||
[Стратегия B] build_deploy_command пробрасывает EXPECTED_REVISION=<sha>
|
||||
в remote-команду хука рядом с SOURCE_IMAGE; формат env корректен (shlex-quote).
|
||||
module: tests/test_deploy_build_once.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: >
|
||||
[Стратегия B] Хук содержит ветку fail-fast: при заданном EXPECTED_REVISION и
|
||||
несовпадении revision лейбла SOURCE_IMAGE -> exit 1 ПЕРЕД docker tag; при пустом
|
||||
EXPECTED_REVISION -> обратносовместимое поведение (без проверки). Статическая
|
||||
проверка текста scripts/orchestrator-deploy-hook.sh (паттерн test_deploy_build_once).
|
||||
module: tests/test_deploy_hook_provenance.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: >
|
||||
[Стратегия B] Dockerfile объявляет ARG GIT_SHA и LABEL
|
||||
org.opencontainers.image.revision=$GIT_SHA (статическая проверка текста Dockerfile).
|
||||
module: tests/test_deploy_hook_provenance.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: >
|
||||
Маппинг контракта: exit 1 хука (fail-fast по провенансу) ->
|
||||
map_exit_code_to_status == "FAILED" (контракт ORCH-36 не изменён).
|
||||
module: tests/test_deploy_hook_mapping.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-10
|
||||
type: integration
|
||||
description: >
|
||||
Stale-образ -> fail-fast end-to-end: на ребре deploy-staging->deploy при
|
||||
несоответствии образа Phase B/хук дают FAILED -> advance_stage откатывает на
|
||||
development (БАГ-8), прод не "зелёный". Прод-рестарт замокан.
|
||||
module: tests/test_stage_engine.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: integration
|
||||
description: >
|
||||
Свежий образ -> happy path: соответствие revision -> деплой доходит до SUCCESS и
|
||||
deploy->done; механизм свежести не блокирует валидный деплой (anti-deadlock, AC-6).
|
||||
Host-процесс/хук замокан.
|
||||
module: tests/test_stage_engine.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: >
|
||||
[Если выбран gate-side] check_staging_image_fresh зарегистрирован в QG_CHECKS;
|
||||
snapshot-тест реестра обновлён и зелёный.
|
||||
module: tests/test_qg_registry_snapshot.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-13
|
||||
type: unit
|
||||
description: >
|
||||
Конфигурация: новые настройки (image_freshness_enabled / image_freshness_repos)
|
||||
читаются с префиксом ORCH_ и имеют дефолты; kill-switch off -> прежнее поведение.
|
||||
module: tests/test_config.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-14
|
||||
type: unit
|
||||
description: >
|
||||
Регрессия контрактов: STAGE_TRANSITIONS (набор стадий) и exit-code-контракт хука
|
||||
(0/1/2) не изменены существующими правками.
|
||||
module: tests/test_stages.py
|
||||
expected: PASS
|
||||
@@ -0,0 +1,209 @@
|
||||
# ADR-001 (ORCH-058): Провенанс staging-образа перед BUILD-ONCE retag в прод
|
||||
|
||||
## Статус
|
||||
Accepted (design) — реализация в ветке `feature/ORCH-058-self-deploy-retag-staging`.
|
||||
Метка: `arch:major-change` (новая deploy-safety модель + новый QG + новый режим хука).
|
||||
|
||||
## Контекст
|
||||
|
||||
ORCH-36 сделал стадию `deploy` исполняемой для self-hosting (`orchestrator`): Phase B
|
||||
(`self_deploy.build_deploy_command`) запускает детачед host-хук, который шагом **2b**
|
||||
(BUILD-ONCE) делает `docker tag $SOURCE_IMAGE → $TARGET_IMAGE` **без `docker build`** —
|
||||
«прод получает ровно тот артефакт, что прошёл staging».
|
||||
|
||||
Дизайн-предпосылка BUILD-ONCE: **staging-образ свеж и собран из провалидированного кода**.
|
||||
На практике этой гарантии НЕТ (BRD §2):
|
||||
|
||||
- Стадия `deploy-staging` запускает только `scripts/staging_check.py` против **уже
|
||||
работающего** контейнера 8501 — что бы в нём ни крутилось. Пересборка staging-образа —
|
||||
ручная операция (STAGING.md / ORCH-34), вне конвейера.
|
||||
- Между «образ собран» и «retag в прод» нет провенанс-связи с провалидированным коммитом.
|
||||
|
||||
Инцидент (LESSONS_ORCH-036 п.4 — **самый опасный** из 4 багов bootstrap): staging-образ
|
||||
не пересобрали из нового `main` → `staging_check` прошёл против СТАРОГО кода → BUILD-ONCE
|
||||
retag промоутнул СТАРЫЙ образ в прод. Деплой «зелёный» (`result=0`, health ok), но прод
|
||||
**молча откатился** на код 2-дневной давности. Орк обслуживает все проекты из одного
|
||||
прод-инстанса → тихий регресс инструмента = групповой инцидент.
|
||||
|
||||
Текущая защита (staging-гейт, merge-gate, health-check хука) этот класс НЕ ловит: все
|
||||
гейты зелёные, потому что проверяют **не тот артефакт**, что уезжает в прод.
|
||||
|
||||
## Инвариант, который нужно обеспечить
|
||||
|
||||
`INV-FRESH` (ТЗ §1): образ, передаваемый хуку как `SOURCE_IMAGE` для BUILD-ONCE retag в
|
||||
прод, собран из ТОГО ЖЕ git-коммита, что прошёл `deploy-staging` для этой задачи. Если
|
||||
это недоказуемо — деплой fail-fast (`deploy_status: FAILED` → откат на `development`,
|
||||
БАГ-8), прод не трогается.
|
||||
|
||||
### Якорь «провалидированного коммита»
|
||||
|
||||
**SHA = `git rev-parse HEAD` в worktree ветки задачи ПОСЛЕ merge-gate** (т.е. после
|
||||
возможного `auto_rebase_onto_main` + `push --force-with-lease`). Это ровно тот tree,
|
||||
который merge-gate ре-тестировал зелёным и который сольётся в `main`. Один helper
|
||||
`validated_revision(repo, branch)` (never-raise) вычисляет SHA и служит ЕДИНСТВЕННЫМ
|
||||
источником и для штампа сборки (Стратегия A), и для ожидаемого ревижна (Стратегия B) —
|
||||
два потребителя одного якоря не могут разойтись.
|
||||
|
||||
## Решение: A + B (defense in depth)
|
||||
|
||||
Ни одна стратегия по отдельности не закрывает задачу:
|
||||
|
||||
- **B в одиночку** (fail-fast по провенансу) делает тихий промоут структурно невозможным,
|
||||
НО если staging-образ устарел — fail-fast'ит **навсегда** (нет пути к зелёному без
|
||||
ручной пересборки) → нарушает BR-5 / AC-6 (deadlock), воспроизводит ровно тот
|
||||
bootstrap-разрыв, который мы устраняем.
|
||||
- **A в одиночку** (пересборка из провалидированного коммита) закрывает петлю «валидируем =
|
||||
промоутим», НО не имеет утверждения В МОМЕНТ retag: гонка/отключение/сбой пересборки
|
||||
снова даст тихий промоут.
|
||||
|
||||
Поэтому берём **обе**, как взаимодополняющие слои:
|
||||
|
||||
### Стратегия A — пересборка staging-образа из провалидированного коммита (liveness, AC-4/AC-6)
|
||||
|
||||
Для self-hosting на ребре `deploy-staging → deploy`, **после merge-gate** (когда
|
||||
валидированный HEAD финализирован) и **до Phase A**, детерминированный код:
|
||||
|
||||
1. Вычисляет `sha = validated_revision(repo, branch)`.
|
||||
2. Пересобирает `orchestrator-orchestrator-staging` из **worktree ветки** (build-context =
|
||||
валидированный tree) с `--build-arg GIT_SHA=<sha>` и пересоздаёт контейнер 8501 на
|
||||
свежем образе (`--no-build`).
|
||||
3. Прогоняет `staging_check.py --mode stub` против свежего 8501.
|
||||
|
||||
Результат: ровно ЭТОТ образ (с лейблом `revision=<sha>`) становится `SOURCE_IMAGE` для
|
||||
прод-retag → петля замкнута, валидируем и промоутим один артефакт (AC-4). Пересборка/
|
||||
recreate трогают **ТОЛЬКО staging (8501)**, НИКОГДА прод (8500) (AC-9).
|
||||
|
||||
Исполнение — через host (ssh, синхронно): docker CLI / compose доступны на ХОСТЕ, не в
|
||||
контейнере (Dockerfile ставит только `openssh-client git`; staging_check уже гоняется
|
||||
`docker exec`-ом на хосте). Новый режим хука `--build-staging` (см. ниже) выполняет сборку
|
||||
и recreate. Синхронный ssh достаточен — рестарт staging не убивает прод-worker (в отличие
|
||||
от Phase B, где нужен detached + finalizer).
|
||||
|
||||
Реализуется как **детерминированный QG-под-чек `check_staging_image_fresh`** (по образцу
|
||||
`check_branch_mergeable`, ORCH-043): pure-условность + never-raise; для прочих репо →
|
||||
`(True, "N/A")`. Регистрируется в `QG_CHECKS` и в `tests/test_qg_registry_snapshot.py`.
|
||||
Вызов — на ребре через `_handle_image_freshness(...)` в `stage_engine` (рядом с
|
||||
`_handle_merge_gate`, ПОСЛЕ него, ДО Phase A). FAIL → откат на `development` + release
|
||||
merge-lease (как merge-gate). **`STAGE_TRANSITIONS` (набор стадий) НЕ меняется** — это
|
||||
под-гейт ребра.
|
||||
|
||||
### Стратегия B — fail-closed провенанс-guard в хуке (safety, AC-1/AC-2/AC-3)
|
||||
|
||||
1. **`Dockerfile`**: `ARG GIT_SHA` + `LABEL org.opencontainers.image.revision=$GIT_SHA`.
|
||||
Без build-arg лейбл пустой → fail-closed на стороне B (см. ниже).
|
||||
2. **`build_deploy_command`**: вычисляет `EXPECTED_REVISION = validated_revision(repo,
|
||||
branch)` и пробрасывает в env команды хука.
|
||||
3. **`orchestrator-deploy-hook.sh` шаг 2b** — ПЕРЕД `docker tag`:
|
||||
- читает лейбл `SOURCE_IMAGE`:
|
||||
`docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$SOURCE_IMAGE"`;
|
||||
- сравнивает с `$EXPECTED_REVISION`;
|
||||
- несовпадение / пустой лейбл / пустой `EXPECTED_REVISION` / ошибка inspect →
|
||||
`log` + `exit 1` (**fail-closed**, никогда не промоутить «на авось»).
|
||||
- **Обратная совместимость:** при НЕзаданном `EXPECTED_REVISION` — текущее поведение
|
||||
(проверка пропускается), чтобы не сломать не-self репо и legacy-вызовы.
|
||||
4. `exit 1` уже маппится `map_exit_code_to_status → FAILED` (контракт не меняется), Phase C
|
||||
пишет `deploy_status: FAILED` → откат на `development` (БАГ-8). Прод не рестартуется на
|
||||
устаревший образ — guard срабатывает ДО `docker tag`/restart.
|
||||
|
||||
### Новый режим хука `--build-staging` (для Стратегии A)
|
||||
|
||||
`orchestrator-deploy-hook.sh --build-staging` (env: `GIT_SHA`, `BUILD_CONTEXT` = host-путь
|
||||
worktree, `TARGET_IMAGE=orchestrator-orchestrator-staging`, `TARGET_SERVICE`,
|
||||
`COMPOSE_PROFILE=staging`, `TARGET_PORT=8501`):
|
||||
`docker build --build-arg GIT_SHA=<sha> -t <TARGET_IMAGE> <BUILD_CONTEXT>` →
|
||||
`docker compose --profile staging up -d --no-build orchestrator-staging` → health 8501.
|
||||
Тот же exit-code-контракт (0=ok). Дефолты режима — STAGING-safe (как у `--deploy`).
|
||||
|
||||
Host-путь build-context выводится из container-пути worktree заменой
|
||||
`repos_dir → host_repos_dir` (как `host_state_dir` в `self_deploy.py`); требуется
|
||||
производный helper host-worktree-пути (или новая настройка `ORCH_HOST_WORKTREES_DIR`).
|
||||
|
||||
## Конфигурация (`src/config.py`, все с префиксом `ORCH_` — урок ORCH-36 п.2)
|
||||
|
||||
- `image_freshness_enabled: bool = True` — **единый** kill-switch ВСЕЙ фичи (A и B вместе).
|
||||
`False` → ни пересборки, ни проброса `EXPECTED_REVISION` → поведение ровно как ORCH-36
|
||||
(BUILD-ONCE без guard). A и B включаются/выключаются **как одно целое**, чтобы не было
|
||||
опасной полу-конфигурации «B без A» (вечный fail-fast).
|
||||
- `image_freshness_repos: str = ""` — CSV; пусто → только self-hosting (как
|
||||
`self_deploy_repos` / `merge_gate_repos`).
|
||||
|
||||
> **Инвариант конфигурации (AC-6):** B активен ТОЛЬКО когда активен A. По умолчанию
|
||||
> (`image_freshness_enabled=True`) валидный деплой всегда доходит до зелёного (A пересобирает
|
||||
> → лейбл == EXPECTED → B пропускает). Полное выключение → legacy ORCH-36 поведение.
|
||||
|
||||
## Порядок на ребре `deploy-staging → deploy` (self-hosting)
|
||||
|
||||
1. `check_staging_status` (существующий) — первичный staging-вердикт агента (smoke,
|
||||
что staging-инфра жива).
|
||||
2. merge-gate `check_branch_mergeable` (существующий) — финализирует валидированный HEAD
|
||||
(rebase если позади, ре-тест зелёный, lease HELD). DEFER на busy-lock → возврат без
|
||||
пересборки.
|
||||
3. **`check_staging_image_fresh` (НОВЫЙ, Стратегия A)** — пересборка из валидированного
|
||||
HEAD + recreate 8501 + `staging_check`. FAIL → откат на `development` + release lease.
|
||||
4. Phase A (существующий) → запрос approve.
|
||||
5. Phase B (human Approved) → `build_deploy_command` с `EXPECTED_REVISION` → хук-guard (B)
|
||||
→ BUILD-ONCE retag только при совпадении → restart прод → Phase C finalizer.
|
||||
|
||||
> Двойной прогон `staging_check` (агент на стадии + код на шаге 3) — **намеренный**: первый
|
||||
> валидирует УЖЕ работающий (потенциально устаревший) 8501 как soft pre-check; авторитетный
|
||||
> — шаг 3 против СВЕЖЕГО образа, который и уедет в прод. `--mode stub` быстр и без LLM-трат.
|
||||
|
||||
## Контракты, которые НЕ меняются (AC-7)
|
||||
|
||||
`STAGE_TRANSITIONS` (набор стадий), exit-code-контракт хука (0/1/2),
|
||||
`map_exit_code_to_status`, `check_deploy_status` / `_parse_deploy_status` (frontmatter-only),
|
||||
БАГ-8 rollback, terminal-sync `deploy → done`, merge-gate (ORCH-43), Phase A/B/C ORCH-36.
|
||||
**Схема БД — без миграций** (состояние свежести не персистится в БД; провенанс живёт в
|
||||
лейбле образа). Добавление `check_staging_image_fresh` в `QG_CHECKS` — ожидаемое расширение
|
||||
реестра (ТЗ §10), не входит в замороженный список AC-7.
|
||||
|
||||
## Last-line-of-defence / fail-closed (AC-2/AC-3)
|
||||
|
||||
Даже если A отключена/проиграла гонку/сбойнула — **B (хук-guard) делает тихий промоут
|
||||
устаревшего образа структурно невозможным**: рассинхрон лейбла и `EXPECTED_REVISION` →
|
||||
`exit 1` ДО retag → FAILED → откат. На любом сомнении (нет лейбла, пустой ожидаемый SHA,
|
||||
ошибка inspect) — трактуется как несоответствие. Прод никогда не трогается «на авось».
|
||||
|
||||
## never-raise (AC-8)
|
||||
|
||||
`validated_revision`, `rebuild_staging_image`, `check_staging_image_fresh`,
|
||||
`build_deploy_command` (проброс EXPECTED) — все защищены try/except, любая ошибка → безопасный
|
||||
вердикт (для A-под-чека: `(False, reason)` с release lease; пустой `EXPECTED_REVISION` на
|
||||
сомнении → B fail-closed). Исключение никогда не всплывает в `stage_engine`.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Плюсы**
|
||||
- Класс «тихого регресса прод» закрыт структурно (B), а валидный деплой всегда доходит до
|
||||
зелёного (A) — bootstrap-разрыв «ручная пересборка staging» устранён.
|
||||
- Валидируем и промоутим один и тот же артефакт (AC-4); провенанс машиночитаем (лейбл).
|
||||
- Единый kill-switch, поэтапный раскат, условность только для self-hosting — без регрессий
|
||||
для не-self репо.
|
||||
|
||||
**Минусы / ограничения**
|
||||
- Латентность ребра растёт: +`docker build` staging + recreate 8501 + повторный
|
||||
`staging_check` перед Phase A. Приемлемо (выполняется в monitor-треде, как merge-gate
|
||||
re-test; bounded timeouts).
|
||||
- `staging_check` гоняется дважды (soft pre-check агента + авторитетный код) — осознанная
|
||||
плата за AC-4. Возможная будущая оптимизация: облегчить шаг 3 до health+revision-smoke,
|
||||
если merge-gate re-test признать достаточным для кода.
|
||||
- Требуется host-доступ к `docker build`/`compose` под slin (как для `--deploy`) и writable
|
||||
build-context (worktree) — заложено инфра-требованиями (07).
|
||||
- Новая под-компонента (QG `check_staging_image_fresh` + режим хука `--build-staging`) →
|
||||
`arch:major-change`.
|
||||
|
||||
## Альтернативы (отклонены)
|
||||
|
||||
- **Только B.** Deadlock без авто-пересборки (BR-5/AC-6). ❌
|
||||
- **Только A.** Нет утверждения в момент retag → гонка/отключение снова даёт тихий промоут
|
||||
(AC-2/AC-3). ❌
|
||||
- **Rebuild в хуке на Phase B (прод-сторона).** Уничтожает BUILD-ONCE (прод-rebuild) и
|
||||
промоутит образ, который staging-e2e никогда не валидировал. ❌
|
||||
- **Rebuild напрямую из контейнера через docker.sock.** В образе нет docker CLI/compose;
|
||||
staging-операции и так host-side (ssh). ❌
|
||||
|
||||
## Связанные ADR
|
||||
Глобальный: `docs/architecture/adr/adr-0008-staging-image-provenance.md`.
|
||||
`adr-0007-executable-self-deploy` (ORCH-36, BUILD-ONCE), `adr-0006-merge-gate` (ORCH-43,
|
||||
образец edge-под-гейта), `adr-0003-staging-gate` (ORCH-35, условность), `adr-0005`
|
||||
(run-as-host-uid).
|
||||
71
docs/work-items/ORCH-058/07-infra-requirements.md
Normal file
71
docs/work-items/ORCH-058/07-infra-requirements.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Инфра-требования — ORCH-058
|
||||
|
||||
Work Item ID: ORCH-058
|
||||
|
||||
Топология не меняется (тот же сервер mva154, те же контейнеры 8500/8501, общая БД). Меняется
|
||||
**что делает self-deploy на ребре `deploy-staging → deploy`** для self-hosting. Полная
|
||||
топология/риски — `docs/operations/INFRA.md` (обновить в том же PR).
|
||||
|
||||
## IR-1. Host-сборка staging-образа (Стратегия A)
|
||||
|
||||
Шаг свежести пересобирает `orchestrator-orchestrator-staging` на ХОСТЕ (docker CLI/compose
|
||||
есть на хосте, НЕ в контейнере — образ ставит только `openssh-client git`). Требуется:
|
||||
|
||||
- Рабочий ssh `slin@127.0.0.1` (уже есть, ORCH-36 / LESSONS п.1–2: passwd-запись uid 1000,
|
||||
ключ смонтирован, `ORCH_DEPLOY_*` префиксы).
|
||||
- На хосте под `slin` доступны `docker build` и `docker compose --profile staging`
|
||||
(recreate 8501). Группа docker (`group_add: "999"` / host-доступ к `docker.sock`) — уже
|
||||
настроено.
|
||||
- **Build-context = host-путь worktree** валидированной ветки
|
||||
(`/home/slin/repos/_wt/<repo>/<branch-slug>`), читаемый под `slin`. Worktree уже
|
||||
создаётся launcher'ом/merge-gate под slin (ADR-0005 run-as-host-uid) — права ок.
|
||||
- Лог-директория хука writable под slin (`/var/log/orchestrator`, LESSONS п.3) — уже.
|
||||
|
||||
## IR-2. Вывод host-пути worktree
|
||||
|
||||
В контейнере worktree виден как `ORCH_WORKTREES_DIR=/repos/_wt/...`; на хосте — как
|
||||
`/home/slin/repos/_wt/...`. Маппинг = замена `repos_dir → host_repos_dir` (как
|
||||
`self_deploy.host_state_dir`). Реализация: производный helper host-worktree-пути, либо новая
|
||||
настройка `ORCH_HOST_WORKTREES_DIR` (дефолт `/home/slin/repos/_wt`). Без неё — деривация из
|
||||
`host_repos_dir`.
|
||||
|
||||
## IR-3. OCI-лейбл происхождения (Стратегия B)
|
||||
|
||||
`Dockerfile`: `ARG GIT_SHA` + `LABEL org.opencontainers.image.revision=$GIT_SHA`. Сборки БЕЗ
|
||||
build-arg (ручные/legacy) дают пустой лейбл → B fail-closed (это by design, не регрессия:
|
||||
прод-retag без доказуемого провенанса должен падать). Любой существующий способ сборки прод/
|
||||
staging-образа (CI, ручной) при включённой фиче ОБЯЗАН передавать `--build-arg GIT_SHA=<sha>`,
|
||||
иначе деплой задачи fail-fast'нется на guard. Шаг A это делает автоматически.
|
||||
|
||||
## IR-4. ssh-режим хука `--build-staging`
|
||||
|
||||
Новый режим `orchestrator-deploy-hook.sh --build-staging` запускается синхронно (рестарт
|
||||
staging безопасен, detached/finalizer не нужны — в отличие от Phase B прод). Дефолты режима —
|
||||
STAGING-safe (`TARGET_PORT=8501`, `--profile staging`). Прод (8500) этим режимом НЕ
|
||||
затрагивается.
|
||||
|
||||
## IR-5. Конфигурация (env, префикс `ORCH_`)
|
||||
|
||||
- `ORCH_IMAGE_FRESHNESS_ENABLED` (дефолт true) — единый kill-switch A+B.
|
||||
- `ORCH_IMAGE_FRESHNESS_REPOS` (дефолт пусто → self-hosting).
|
||||
- (опц.) `ORCH_HOST_WORKTREES_DIR` (дефолт `/home/slin/repos/_wt`).
|
||||
|
||||
`EXPECTED_REVISION` для хука строится в `build_deploy_command` — отдельной настройки не
|
||||
требует. `deploy_prod_source_image` (= `orchestrator-orchestrator-staging`) переиспользуется.
|
||||
|
||||
## IR-6. Безопасность self-hosting (инварианты)
|
||||
|
||||
- Любые `docker build` / `compose up` / recreate — ТОЛЬКО staging (8501); прод (8500) не
|
||||
рестартуется в рамках шага свежести.
|
||||
- `main` не пушится; force-only — `--force-with-lease` на ветку задачи (merge-gate, без
|
||||
изменений). Шаг A не пушит ничего (только локальный `docker build`).
|
||||
- B-guard срабатывает ДО `docker tag`/restart — прод не трогается на сомнении.
|
||||
|
||||
## IR-7. Bootstrap-чеклист (урок ORCH-36 «сквозной»)
|
||||
|
||||
Перед мержем ORCH-058 — **реальный** прогон в staging-петле (не только бумажные гейты):
|
||||
сборка staging из worktree с GIT_SHA → лейбл присутствует
|
||||
(`docker image inspect ... revision`) → recreate 8501 → `staging_check` зелёный →
|
||||
`build_deploy_command` отдаёт непустой `EXPECTED_REVISION` → хук-guard пропускает при
|
||||
совпадении и `exit 1` при подмене `SOURCE_IMAGE` на устаревший. Зафиксировать в bootstrap-
|
||||
заметке (как LESSONS_ORCH-036).
|
||||
16
docs/work-items/ORCH-058/10-tech-risks.md
Normal file
16
docs/work-items/ORCH-058/10-tech-risks.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Технические риски — ORCH-058
|
||||
|
||||
Work Item ID: ORCH-058
|
||||
|
||||
| ID | Риск | Вероятность / Влияние | Митигация |
|
||||
|----|------|----------------------|-----------|
|
||||
| R-1 | **Полу-конфигурация «B без A»** → вечный fail-fast деплоя (B падает, никто не пересобирает) | Низк. / Высок. (deadlock, BR-5) | Единый kill-switch `image_freshness_enabled` включает/выключает A и B **как целое**; раздельных флагов A/B нет. Дефолт — оба включены. AC-6. |
|
||||
| R-2 | **Рассинхрон якоря**: merge-gate делает rebase ПОСЛЕ того, как агент прогнал staging_check → HEAD изменился | Сред. / Сред. | Якорь берётся ПОСЛЕ merge-gate; шаг A пересобирает из post-rebase HEAD; авторитетный staging_check — против свежего образа. Pre-check агента — soft. |
|
||||
| R-3 | **Гонка**: между пересборкой A и Phase B human-approve worktree HEAD сместился | Низк. / Высок. | B сверяет лейбл образа с `EXPECTED_REVISION`=validated_revision на момент Phase B; рассинхрон → fail-closed `exit 1`, прод не трогается. AC-2/AC-3. |
|
||||
| R-4 | **Пустой лейбл** (ручная/legacy/CI-сборка без `--build-arg GIT_SHA`) | Сред. / Высок. | Fail-closed: пустой лейбл → несоответствие → `exit 1`. By design. Шаг A всегда передаёт GIT_SHA. IR-3 фиксирует требование к любым сборкам. |
|
||||
| R-5 | **Латентность ребра**: +docker build staging +recreate +повторный staging_check перед approve | Высок. / Низк. | Bounded timeouts; выполняется в monitor-треде (как merge-gate re-test). `staging_check --mode stub` без LLM-трат. Приемлемо. |
|
||||
| R-6 | **Сборка/recreate случайно затронет прод (8500)** | Низк. / Критич. | Режим хука `--build-staging` со STAGING-safe дефолтами (8501, `--profile staging`); код шага A никогда не передаёт прод-параметры. AC-9. Тест-инвариант: цель != прод. |
|
||||
| R-7 | **docker build на хосте падает** (нет места, недоступен daemon, битый worktree) | Низк. / Сред. | never-raise: `check_staging_image_fresh` → `(False, reason)` + release lease → откат на `development` (не зависание, не тихий промоут). AC-8. |
|
||||
| R-8 | **Двойной staging_check** воспринят как баг/лишняя трата | Сред. / Низк. | Документировано как намеренное (soft pre-check агента vs авторитетный код против промоутимого образа). Будущая оптимизация — облегчить шаг A. |
|
||||
| R-9 | **Самохостинг-bootstrap**: фича не действует, пока сама не в проде (старый прод-образ без лейбла) | Высок. (однократно) / Сред. | Bootstrap-чеклист (IR-7): первый реальный staging-прогон + ручной разрыв; B обратносовместим (без `EXPECTED_REVISION` — старое поведение), раскат поэтапный через флаг. |
|
||||
| R-10 | **Деградация не-self репо** | Низк. / Высок. | Условность (`image_freshness_repos` пусто → только orchestrator); для прочих — `(True, "N/A")` + хук без `EXPECTED_REVISION` = прежний путь. AC-5. |
|
||||
131
docs/work-items/ORCH-058/15-staging-log.md
Normal file
131
docs/work-items/ORCH-058/15-staging-log.md
Normal file
@@ -0,0 +1,131 @@
|
||||
---
|
||||
staging_status: FAILED
|
||||
timestamp: 2026-06-07T11:01:00Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log — ORCH-058
|
||||
|
||||
Staging test suite ran against the live staging environment and **FAILED** (exit code `1`,
|
||||
**8/10 checks PASS**). Block C (E2E) checks C9a and C9b failed.
|
||||
|
||||
Per the staging-gate contract this is the machine verdict `FAILED` (it reflects the real suite
|
||||
exit code, never an LLM declaration). Smoke (A1–A3) and access (B4–B6) all passed, **including
|
||||
B6 registry isolation** — so this is NOT a B6/ORCH-048 false-FAIL.
|
||||
|
||||
> ⚠️ **CORRECTED ROOT CAUSE — read before acting on this rollback.** The previous revision of
|
||||
> this log blamed `handle_status_start` / a regression in the validated artifact. **That was
|
||||
> wrong**, which is why the dev↔staging cycle kept repeating. Direct inspection inside the
|
||||
> running staging instance proves the production code is **correct** and the failure is a bug in
|
||||
> the **test harness `scripts/staging_check.py`**. Do NOT touch `src/webhooks/plane.py` /
|
||||
> `handle_status_start` / any ORCH-058 image-freshness code. **Fix `scripts/staging_check.py`.**
|
||||
|
||||
## Execution
|
||||
- Canonical `docker exec` into `orchestrator-staging` (ORCH-048, ADR-001), invoked via the
|
||||
Docker Engine API over the mounted unix socket (the `docker` CLI binary is absent in the
|
||||
agent runtime image; the Engine-API exec is the exact equivalent of
|
||||
`docker exec orchestrator-staging python3 /repos/orchestrator/scripts/staging_check.py
|
||||
--base-url http://localhost:8501 --mode stub`).
|
||||
- Script: `/repos/orchestrator/scripts/staging_check.py` (bind-mount, served from the host repo,
|
||||
NOT baked into the image — so a harness fix takes effect on the next run without a rebuild).
|
||||
- Mode: `stub`
|
||||
- Exit code: `1`
|
||||
- Result: **8/10 checks PASS** (FAIL: C9a, C9b)
|
||||
- Staging image under test: `orchestrator-orchestrator-staging`, OCI label
|
||||
`org.opencontainers.image.revision=094b5e2f960f696216f8661ff9c27b0d4706f219` (= the **merge
|
||||
commit of ORCH-058 into `main`**, PR #57; ancestor of branch HEAD `60e5596e`). Container
|
||||
recreated 2026-06-07T10:13:36Z. So the artifact under test genuinely contains the validated
|
||||
ORCH-058 code.
|
||||
|
||||
## Decisive root cause (proven, actionable)
|
||||
Block C creates a SANDBOX Plane issue (C7 ✓), then POSTs a signed `/webhook/plane` payload to
|
||||
start the pipeline (C8 ✓ — HTTP 200 `{"status":"accepted"}`). The staging instance logged for
|
||||
the test issue `427cb94e-…`:
|
||||
|
||||
```
|
||||
2026-06-07 10:59:04 [INFO] orchestrator.webhooks.plane: issue 427cb94e-cedd-4def-ba5d-21c555a82477
|
||||
updated to state b873d9eb..., no pipeline action
|
||||
```
|
||||
|
||||
`handle_issue_updated` (src/webhooks/plane.py) starts the pipeline **only** when the webhook's
|
||||
new state equals the **incoming project's** `in_progress` state, resolved per-project from the
|
||||
Plane API by `get_project_states(project_id)` (ORCH-10). The webhook the harness sends carries
|
||||
state `b873d9eb-993c-48cd-97ac-99a9b1623967`.
|
||||
|
||||
**The mismatch (queried live inside the staging container):**
|
||||
|
||||
| | UUID |
|
||||
|---|---|
|
||||
| `staging_check.py` `IN_PROGRESS_STATE_ID` (hardcoded) | `b873d9eb-993c-48cd-97ac-99a9b1623967` |
|
||||
| `get_project_states(SANDBOX)["in_progress"]` (real) | `84a76f65-75f8-4022-9554-379dad38523c` |
|
||||
| `_DEFAULT_STATES["in_progress"]` (enduro-trails fallback) | `b873d9eb-993c-48cd-97ac-99a9b1623967` |
|
||||
|
||||
The hardcoded `b873d9eb…` is the **enduro-trails** In Progress UUID (the `_DEFAULT_STATES`
|
||||
fallback), **not** SANDBOX's. SANDBOX's actual In Progress is `84a76f65…`. So the handler
|
||||
**correctly** classifies the enduro-state webhook as `no pipeline action` for a SANDBOX issue →
|
||||
no `tasks` row, no Gitea branch (C9a FAIL after 60s), no analyst job enqueued (C9b FAIL).
|
||||
Cleanup confirmed `no task row found` and `no branch to delete`.
|
||||
|
||||
**Why it intermittently "passed 10/10" before (09:31):** `get_project_states` falls back to
|
||||
`_DEFAULT_STATES` (= `b873d9eb…`) whenever the Plane states API call fails / returns no
|
||||
recognisable states. On runs where that fallback fired, the hardcoded harness state accidentally
|
||||
matched and the pipeline started. On this run the SANDBOX states API call succeeded at startup
|
||||
(`GET …/projects/8c5a3025-…/states/ → 200 OK`), so SANDBOX resolved to its real `84a76f65…` and
|
||||
the accidental match disappeared. The green runs were the bug; the red runs are correct handler
|
||||
behaviour exposing a harness that hardcodes the wrong project's state.
|
||||
|
||||
## Required fix (for the development rollback) — in `scripts/staging_check.py` ONLY
|
||||
Make the E2E harness send SANDBOX's **actual** `in_progress` state instead of a hardcoded enduro
|
||||
UUID. Resolve it dynamically the same way the app does — e.g. `GET
|
||||
/workspaces/<slug>/projects/<SANDBOX_PROJECT_ID>/states/`, pick the state whose `name` is
|
||||
`"In Progress"` (group `"started"`), and use its `id` in `_make_webhook_payload`. (The harness
|
||||
already calls the Plane API for B4/B6, so credentials/URL are available.) Do **not** rely on the
|
||||
`_DEFAULT_STATES` fallback coincidence. No production-code change is warranted; ORCH-058's
|
||||
image-provenance feature is unaffected by this and is functioning.
|
||||
|
||||
## Test output
|
||||
|
||||
```
|
||||
============================================================
|
||||
ORCH-33 Staging Check Suite
|
||||
base_url : http://localhost:8501
|
||||
mode : stub
|
||||
utc_time : 2026-06-07T10:59:02.392888+00:00
|
||||
============================================================
|
||||
|
||||
[Block A] SMOKE
|
||||
✓ PASS A1 GET /health → 200 status=ok [HTTP 200, body={'status': 'ok', 'service': 'orchestrator'}]
|
||||
✓ PASS A2 GET /queue → 200 with counts/max_concurrency/resilience [HTTP 200, keys=['counts', 'max_concurrency', 'poll_interval', 'resilience', 'reconcile', 'recent']]
|
||||
✓ PASS A3 ORCH_STAGING=true (not prod) [ORCH_STAGING=true]
|
||||
|
||||
[Block B] ACCESS
|
||||
✓ PASS B4 Plane: sandbox project accessible [HTTP 200, found 5 project(s), sandbox=YES]
|
||||
✓ PASS B5 Gitea: orchestrator-sandbox accessible, push=true [HTTP 200, permissions={'admin': True, 'push': True, 'pull': True}]
|
||||
✓ PASS B6 Registry: sandbox present, prod ET/ORCH absent [sandbox=YES, prod-ET=NO(good), prod-ORCH=NO(good)]
|
||||
|
||||
[Block C] E2E (mode=stub)
|
||||
· C7: Creating issue in SANDBOX project...
|
||||
✓ PASS C7 Create issue in Plane SANDBOX [HTTP 201, issue_id=427cb94e-cedd-4def-ba5d-21c555a82477]
|
||||
· C8: Triggering pipeline via POST /webhook/plane ...
|
||||
· Using HMAC signature (secret len=40)
|
||||
✓ PASS C8 Trigger pipeline via /webhook/plane [HTTP 200, resp={'status': 'accepted'}]
|
||||
· C9a: Polling for branch in orchestrator-sandbox (up to 60s)...
|
||||
· waiting... (waiting for branch) [×20]
|
||||
✗ FAIL C9a Branch appears in orchestrator-sandbox [branch=not found]
|
||||
· C9b: Checking staging job queue for analyst job (up to 30s)...
|
||||
· (Plane comment check skipped: bot-tokens not added to SANDBOX project)
|
||||
· waiting... (waiting for analyst job in queue) [×15]
|
||||
✗ FAIL C9b Analyst job enqueued in staging queue
|
||||
|
||||
[CLEANUP]
|
||||
· CLEANUP: no branch to delete
|
||||
✓ PASS CLEANUP: deleted Plane issue 427cb94e-cedd-4def-ba5d-21c555a82477 (HTTP 204)
|
||||
· CLEANUP DB: no task row found for plane_id=427cb94e-cedd-4def-ba5d-21c555a82477
|
||||
· CLEANUP DB dedup: no such table: events_dedup
|
||||
|
||||
============================================================
|
||||
RESULT: 8/10 checks PASS
|
||||
============================================================
|
||||
```
|
||||
|
||||
EXIT_CODE=1
|
||||
29
docs/work-items/ORCH-059/15-staging-log.md
Normal file
29
docs/work-items/ORCH-059/15-staging-log.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-07T19:19:25Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed. Verdict: **SUCCESS** (exit 0).
|
||||
|
||||
Canonical run inside the `orchestrator-staging` container (ORCH-048, ADR-001):
|
||||
`python3 /repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub`
|
||||
|
||||
## Result
|
||||
|
||||
- RESULT: 8/10 checks PASS
|
||||
- REAL failed: none
|
||||
- SANDBOX_INFRA failed: C9a (branch in orchestrator-sandbox), C9b (analyst job enqueued)
|
||||
|
||||
All REAL pipeline checks (Block A SMOKE, Block B ACCESS incl. B6 registry isolation,
|
||||
C7/C8) are green. The two failing checks are sandbox-infra-only (SANDBOX bot accounts
|
||||
not members of the SANDBOX Plane project) and were waived per ORCH-061. Exit code 0.
|
||||
|
||||
```
|
||||
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
|
||||
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
|
||||
```
|
||||
|
||||
tolerance: staging_infra_tolerance_enabled=True
|
||||
7
docs/work-items/ORCH-060/00-business-request.md
Normal file
7
docs/work-items/ORCH-060/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: Reconciler не должен трогать escalated / max-retries задачи
|
||||
|
||||
Work Item ID: ORCH-060
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
90
docs/work-items/ORCH-060/01-brd.md
Normal file
90
docs/work-items/ORCH-060/01-brd.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# BRD: Reconciler не должен трогать escalated / max-retries задачи
|
||||
|
||||
Work Item ID: ORCH-060
|
||||
Стадия: analysis → architecture
|
||||
Связано: ORCH-053 (reconciler), ORCH-046 (retry-счётчик), ORCH-047 (BLOCKED-вердикт)
|
||||
|
||||
## 1. Контекст и проблема
|
||||
|
||||
ORCH-053 ввёл фоновый reconciler (`src/reconciler.py`) — sweeper, доигрывающий
|
||||
пропущенные webhook-переходы. Слой F-1 (`reconcile_gate_once` →
|
||||
`_reconcile_gate_task`) для каждой не-терминальной задачи (`stage != 'done'`) без
|
||||
активного job и старше grace делает read-only пред-оценку канонического QG; если
|
||||
гейт зелёный → `advance_if_gate_passed` → `advance_stage(..., finished_agent=None)`.
|
||||
|
||||
**Дефект.** Задача, исчерпавшая лимит developer-ретраев
|
||||
(`_developer_retry_count(task_id) >= MAX_DEVELOPER_RETRIES = 3`), **escalated** —
|
||||
но эскалация в обработчиках Gitea (`src/webhooks/gitea.py:280` для CI-failure,
|
||||
`:371` для review REQUEST_CHANGES) выполняет ТОЛЬКО `notify_error(...)`:
|
||||
|
||||
- стадия НЕ меняется (остаётся `development`);
|
||||
- терминального маркера в БД нет (нет `blocked`-флага в таблице `tasks`);
|
||||
- активного job нет.
|
||||
|
||||
Для reconciler такая задача неотличима от «застрявшей из-за потерянного webhook».
|
||||
Если CI к этому моменту зелёный (типичный кейс: разработчик починил CI, но reviewer
|
||||
продолжал слать REQUEST_CHANGES → ушли в лимит), F-1 каждые `reconcile_interval_s`
|
||||
(120 с) видит зелёный `check_ci_green` и **разблокирует** задачу `development → review`.
|
||||
Reviewer снова REQUEST_CHANGES → откат на `development` → снова эскалация (стадия
|
||||
не меняется). Следующий тик — снова разблокировка. Бесконечный цикл.
|
||||
|
||||
**Реальный инцидент (наблюдение 06–07.06.2026).** ET-013 разблокирована
|
||||
reconciler'ом **10 раз за ночь**, в итоге всё равно escalated — бесполезный поллинг
|
||||
каждые 2 минуты, лишние запуски агентов (токены, деньги), шум в Telegram
|
||||
(`reconcile_notify_unblock`), нагрузка на конвейер общего инстанса (self-hosting:
|
||||
один инстанс обслуживает ORCH + enduro-trails).
|
||||
|
||||
Симметричный риск: задача, которую человек/агент явно перевёл в Plane-статус
|
||||
**Blocked** или **Needs Input** (ручной гейт), не должна автоматически
|
||||
разблокироваться reconciler'ом до вмешательства человека.
|
||||
|
||||
## 2. Бизнес-цель
|
||||
|
||||
Reconciler (F-1) обязан **пропускать** (не трогать) задачи, которые:
|
||||
1. исчерпали лимит developer-ретраев (`_developer_retry_count >= MAX_DEVELOPER_RETRIES`), и/или
|
||||
2. находятся в явном «человеческом»/терминальном Plane-статусе **Blocked** / **Needs Input**.
|
||||
|
||||
Такие задачи ждут ручного вмешательства; автоматический sweeper их игнорирует.
|
||||
|
||||
## 3. Заинтересованные стороны
|
||||
|
||||
- **Owner проекта** — прекращение «фантомной» активности и шума по escalated-задачам.
|
||||
- **Другие проекты на инстансе (enduro-trails)** — снижение паразитной нагрузки общей очереди.
|
||||
- **Агенты-разработчики оркестратора** — корректная семантика терминального состояния.
|
||||
|
||||
## 4. Объём (Scope)
|
||||
|
||||
### Входит
|
||||
- Гард в F-1 (`_reconcile_gate_task` / `advance_if_gate_passed`), который ДО
|
||||
оценки гейта и вызова `advance_stage` пропускает escalated-задачи
|
||||
(retry-count >= лимит) — детерминированно, без сети.
|
||||
- Гард, пропускающий задачи в Plane-статусе Blocked / Needs Input.
|
||||
- Тесты (unit) на оба условия + регресс happy-path и отсутствия спама/нотификаций.
|
||||
- Обновление документации: `docs/architecture/README.md` (описание F-1),
|
||||
per-work-item ADR, `CHANGELOG.md`.
|
||||
|
||||
### Не входит
|
||||
- Изменение порога `MAX_DEVELOPER_RETRIES` или логики самой эскалации в `gitea.py`.
|
||||
- Изменение F-2 plane-side по существу (F-2 уже реагирует только на
|
||||
in_progress/approved/rejected, то есть Blocked/Needs Input им не доигрываются —
|
||||
достаточно регресс-теста, фиксирующего это поведение).
|
||||
- Реестры `STAGE_TRANSITIONS` / `QG_CHECKS`, схема прочих стадий.
|
||||
|
||||
## 5. Допущения и ограничения
|
||||
|
||||
- **Инвариант reconciler (ORCH-053):** схема БД и реестры не меняются. Решение
|
||||
должно либо обойтись без миграции, либо архитектор обязан явно обосновать
|
||||
необходимость нового столбца как терминального маркера.
|
||||
- **Never-raise:** гард не должен ломать тик; любая ошибка вычисления условия →
|
||||
безопасный фоллбэк (не трогать задачу — консервативно).
|
||||
- **self-hosting:** нельзя ронять/рестартить прод-контейнер; изменение — чисто
|
||||
логика sweeper'а, деплой через staging (8501) по канону.
|
||||
- Источник истины по retry — `agent_runs` (как у `_developer_retry_count`).
|
||||
|
||||
## 6. Критерий успеха (бизнес)
|
||||
|
||||
После выката на конкретной escalated-задаче (как ET-013): за ночь — **0**
|
||||
строк `reconciler: <wi> ... разблокирована`, **0** повторных запусков агентов,
|
||||
**0** Telegram-нотификаций разблокировки; задача спокойно ждёт человека в
|
||||
`development`/Blocked. При этом штатные «честно застрявшие» задачи
|
||||
(retry < лимита, не Blocked) reconciler по-прежнему доигрывает.
|
||||
113
docs/work-items/ORCH-060/02-trz.md
Normal file
113
docs/work-items/ORCH-060/02-trz.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# ТЗ: Reconciler пропускает escalated / max-retries / blocked-needs-input задачи
|
||||
|
||||
Work Item ID: ORCH-060
|
||||
Стадия: analysis → architecture (архитектор фиксирует механику в ADR)
|
||||
|
||||
## 1. Задействованные модули `src/`
|
||||
|
||||
| Модуль | Роль в задаче |
|
||||
|--------|---------------|
|
||||
| `src/reconciler.py` | **Основное изменение.** F-1: `Reconciler._reconcile_gate_task` — добавить пред-проверки (escalated / blocked / needs-input) ДО `advance_if_gate_passed`. |
|
||||
| `src/stage_engine.py` | Источник `MAX_DEVELOPER_RETRIES` (=3) и `_developer_retry_count(task_id)`. Кандидат на промоут приватного хелпера в переиспользуемый (решает архитектор). |
|
||||
| `src/db.py` | Чтение состояния задачи (`get_active_tasks_for_reconcile` уже отдаёт строки `tasks`); возможный новый read-helper для retry-count, если решено не импортировать приватный из stage_engine. |
|
||||
| `src/plane_sync.py` | Маппинг Plane-статусов (`PLANE_STATES`, `get_project_states`): `blocked`, `needs_input`. Источник для проверки «человеческого» статуса, если архитектор выберет проверку через Plane API. |
|
||||
| `src/webhooks/gitea.py` | НЕ меняется (только справочно: точки эскалации `:280`, `:371`). |
|
||||
|
||||
## 2. Требуемое поведение (контракт F-1)
|
||||
|
||||
`Reconciler._reconcile_gate_task(task)` ДО вызова `advance_if_gate_passed(...)`
|
||||
обязан вернуться (пропустить задачу, ничего не делая, не инкрементируя
|
||||
`unblocked_total`, не слать нотификации), если выполнено ЛЮБОЕ из условий:
|
||||
|
||||
1. **Escalated по ретраям (обязательно, детерминированно, без сети):**
|
||||
`developer_retry_count(task_id) >= MAX_DEVELOPER_RETRIES`.
|
||||
- `MAX_DEVELOPER_RETRIES` импортируется из `stage_engine` (НЕ хардкодить число).
|
||||
- Источник счётчика — тот же запрос, что в `_developer_retry_count`:
|
||||
`SELECT COUNT(*) FROM agent_runs WHERE task_id=? AND agent='developer'`.
|
||||
|
||||
2. **Явный человеческий/терминальный Plane-статус:** issue в состоянии
|
||||
**Blocked** или **Needs Input**.
|
||||
|
||||
Порядок: проверки добавляются в `_reconcile_gate_task` ПОСЛЕ существующих гардов
|
||||
(`stage=='analysis'` carve-out, `get_qg_for_stage is None`, `has_active_job_for_task`,
|
||||
grace) и ДО `advance_if_gate_passed`. Условие (1) — дешёвое (локальный SQL) —
|
||||
проверять раньше условия (2), если (2) требует сети.
|
||||
|
||||
## 3. Механика проверки blocked/needs-input (выбор — за архитектором, ADR)
|
||||
|
||||
В таблице `tasks` НЕТ столбца статуса (`stage` всегда `development` у escalated).
|
||||
Архитектор выбирает и обосновывает один из вариантов; требования к каждому:
|
||||
|
||||
- **Вариант A — проверка через Plane API (без миграции, предпочтительно по
|
||||
инварианту ORCH-053 «схема не меняется»):** для кандидата F-1 запросить текущее
|
||||
состояние issue (per-project `get_project_states` → сверка с `blocked`/`needs_input`).
|
||||
Допустимо, т.к. F-1 уже делает сетевой вызов в гейте (`check_ci_green`), а
|
||||
кандидатов после grace+no-active-job немного. Обязателен never-raise: ошибка
|
||||
запроса → консервативно НЕ трогать задачу (skip), либо явно обоснованный фоллбэк.
|
||||
- **Вариант B — локальный терминальный маркер в БД:** идемпотентная миграция
|
||||
(`tasks.blocked`/`tasks.reconcile_skip`), выставляется в точках `set_issue_blocked`/
|
||||
`set_issue_needs_input` и в точках эскалации `gitea.py`. Требует обоснования
|
||||
нарушения инварианта «схема reconciler не меняется» и затрагивает больше точек.
|
||||
|
||||
> Рекомендация аналитика: условие (1) полностью закрывает зафиксированный инцидент
|
||||
> (ET-013 = escalated = max retries) детерминированно и без сети — оно
|
||||
> обязательно к реализации. Условие (2) — защита от автоперекрытия ручного гейта;
|
||||
> минимально-инвазивный путь — Вариант A. Архитектор вправе ограничить (2)
|
||||
> Вариантом A либо обосновать B.
|
||||
|
||||
## 4. Изменения API
|
||||
|
||||
Нет. Эндпоинты не добавляются и не меняются. Снимок `GET /queue` (блок `reconcile`)
|
||||
по содержимому не меняется; опционально архитектор может добавить best-effort
|
||||
счётчик `skipped_escalated` (необязательно, вне scope AC).
|
||||
|
||||
## 5. Изменения схемы БД
|
||||
|
||||
По умолчанию — **нет** (Вариант A). При выборе Варианта B — идемпотентная
|
||||
ALTER-миграция через `_ensure_column` (как остальные в `db.init_db`),
|
||||
restart-safe, безопасная на живой прод-БД; обязательна явная мотивация в ADR.
|
||||
|
||||
## 6. Требования к QG checks
|
||||
|
||||
Нет новых QG. Реестр `QG_CHECKS` и `STAGE_TRANSITIONS` не меняются. Гард —
|
||||
ВНЕ гейта: он решает, ЗАПУСКАТЬ ли пред-оценку гейта вообще, а не меняет вердикт
|
||||
гейта.
|
||||
|
||||
## 7. Инварианты, которые нельзя нарушить
|
||||
|
||||
- **Never-raise** на единицу работы (per-task `try/except` в `reconcile_gate_once`
|
||||
сохраняется; новая логика не должна бросать наружу).
|
||||
- **Тишина при пропуске:** пропущенная задача не инкрементирует `unblocked_total`,
|
||||
не пишет лог `разблокирована`, не шлёт Telegram.
|
||||
- **Регресс F-1 happy-path:** задача с retry < лимита и не-Blocked/Needs-Input при
|
||||
зелёном гейте по-прежнему доигрывается (`advance_stage` вызывается).
|
||||
- **F-2** по существу не меняется: Blocked/Needs Input не входят в
|
||||
{in_progress, approved, rejected} → не доигрываются (зафиксировать регресс-тестом).
|
||||
- `analysis` carve-out F-1 сохраняется.
|
||||
- Kill-switch'и (`reconcile_enabled`, `reconcile_plane_enabled`) работают как прежде.
|
||||
|
||||
## 8. Артефакты pipeline, которые должны быть созданы/обновлены
|
||||
|
||||
- `docs/work-items/ORCH-060/06-adr/ADR-001-*.md` — решение по механике (2) (A vs B).
|
||||
- `docs/architecture/README.md` — дополнить описание F-1 («skip escalated /
|
||||
blocked / needs-input»).
|
||||
- `CHANGELOG.md` — запись `fix(reconciler): ...`.
|
||||
- Тесты — `tests/test_reconciler.py` (расширение).
|
||||
- Обновить footer `docs/architecture/README.md` (статус ORCH-060).
|
||||
|
||||
## 9. Точки изменения кода (конкретно)
|
||||
|
||||
1. `src/reconciler.py`, `_reconcile_gate_task`: после grace-проверки и до
|
||||
`advance_if_gate_passed` вставить:
|
||||
```python
|
||||
# ORCH-060: escalated tasks (max developer retries reached) are terminal —
|
||||
# they wait for a human, not the sweeper. Skip deterministically (no network).
|
||||
if developer_retry_count(task_id) >= MAX_DEVELOPER_RETRIES:
|
||||
return
|
||||
# ORCH-060: respect an explicit human gate (Blocked / Needs Input).
|
||||
if self._is_blocked_or_needs_input(task): # mechanism per ADR (Variant A/B)
|
||||
return
|
||||
```
|
||||
2. `src/reconciler.py`: импорт `MAX_DEVELOPER_RETRIES` (и retry-count хелпера) из
|
||||
`stage_engine` (или новый read-helper в `db.py`).
|
||||
3. Хелпер проверки Plane-статуса (`_is_blocked_or_needs_input`) — never-raise.
|
||||
124
docs/work-items/ORCH-060/03-acceptance-criteria.md
Normal file
124
docs/work-items/ORCH-060/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Критерии приёмки: ORCH-060
|
||||
|
||||
Work Item ID: ORCH-060
|
||||
|
||||
Формат: каждый критерий — Дано / Когда / Тогда, с однозначным PASS/FAIL.
|
||||
|
||||
---
|
||||
|
||||
## AC-1 — Escalated-задача (retry == лимит) не разблокируется (главный кейс ET-013)
|
||||
|
||||
- **Дано:** задача на `stage='development'`, без активного job, `age >= grace`,
|
||||
`check_ci_green` зелёный; в `agent_runs` ровно `MAX_DEVELOPER_RETRIES` (=3)
|
||||
записей `agent='developer'`.
|
||||
- **Когда:** выполняется `Reconciler.reconcile_gate_once()`.
|
||||
- **Тогда:** стадия остаётся `development`; `advance_stage`/`advance_if_gate_passed`
|
||||
не приводит к смене стадии; `unblocked_total == 0`; новый developer/reviewer job
|
||||
не создаётся.
|
||||
- **PASS:** стадия не изменилась И `unblocked_total == 0` И нет новых job.
|
||||
- **FAIL:** стадия стала `review` / появился новый job / `unblocked_total > 0`.
|
||||
|
||||
## AC-2 — Граница: retry > лимита тоже пропускается
|
||||
|
||||
- **Дано:** то же, но developer-записей `> MAX_DEVELOPER_RETRIES` (например 4–5).
|
||||
- **Когда:** `reconcile_gate_once()`.
|
||||
- **Тогда:** задача пропущена (как AC-1).
|
||||
- **PASS / FAIL:** как AC-1.
|
||||
|
||||
## AC-3 — Регресс happy-path: retry < лимита по-прежнему доигрывается
|
||||
|
||||
- **Дано:** `development`, без активного job, `age >= grace`, `check_ci_green`
|
||||
зелёный; developer-записей `< MAX_DEVELOPER_RETRIES` (например 0, 1 или 2).
|
||||
- **Когда:** `reconcile_gate_once()`.
|
||||
- **Тогда:** задача доигрывается `development → review`; `unblocked_total == 1`;
|
||||
enqueue следующего агента происходит как раньше.
|
||||
- **PASS:** стадия стала `review` И `unblocked_total == 1`.
|
||||
- **FAIL:** задача пропущена / стадия не изменилась.
|
||||
|
||||
## AC-4 — Граница ровно на лимите (==3) → skip, на (лимит−1) → advance
|
||||
|
||||
- **Дано:** две задачи-близнеца, идентичные кроме числа developer-записей:
|
||||
одна с `MAX_DEVELOPER_RETRIES`, другая с `MAX_DEVELOPER_RETRIES − 1`.
|
||||
- **Когда:** `reconcile_gate_once()`.
|
||||
- **Тогда:** первая пропущена (skip), вторая доиграна (advance).
|
||||
- **PASS:** ровно одна из двух доиграна (та, что `−1`).
|
||||
- **FAIL:** обе доиграны / обе пропущены / доиграна задача на лимите.
|
||||
|
||||
## AC-5 — Plane-статус Blocked → пропуск
|
||||
|
||||
- **Дано:** задача-кандидат F-1 (stage не-терминальный, без активного job,
|
||||
`age >= grace`, гейт зелёный), у которой текущий Plane-статус issue = **Blocked**;
|
||||
retry < лимита (чтобы изолировать именно этот гард).
|
||||
- **Когда:** `reconcile_gate_once()`.
|
||||
- **Тогда:** задача пропущена; стадия не меняется; `unblocked_total == 0`.
|
||||
- **PASS:** стадия не изменилась И `unblocked_total == 0`.
|
||||
- **FAIL:** задача доиграна.
|
||||
|
||||
## AC-6 — Plane-статус Needs Input → пропуск
|
||||
|
||||
- **Дано:** как AC-5, но Plane-статус = **Needs Input**.
|
||||
- **Когда:** `reconcile_gate_once()`.
|
||||
- **Тогда:** задача пропущена (как AC-5).
|
||||
- **PASS / FAIL:** как AC-5.
|
||||
|
||||
## AC-7 — Тишина при пропуске (no spam)
|
||||
|
||||
- **Дано:** escalated-задача (как AC-1).
|
||||
- **Когда:** `reconcile_gate_once()` (один или несколько тиков).
|
||||
- **Тогда:** НЕ вызывается `_note_unblock`; нет лог-строки `... разблокирована`;
|
||||
нет `send_telegram`; нет `notify_qg_failure` (пропуск — раньше оценки гейта).
|
||||
- **PASS:** ни одна из перечисленных нотификаций не вызвана.
|
||||
- **FAIL:** вызвана любая нотификация.
|
||||
|
||||
## AC-8 — Никакого сетевого вызова гейта на escalated-задаче
|
||||
|
||||
- **Дано:** escalated-задача (как AC-1) с замоканным `check_ci_green`.
|
||||
- **Когда:** `reconcile_gate_once()`.
|
||||
- **Тогда:** `check_ci_green` (через `advance_if_gate_passed`/`_run_qg`) НЕ
|
||||
вызывается для этой задачи — пропуск происходит раньше.
|
||||
- **PASS:** мок гейта не вызван.
|
||||
- **FAIL:** мок гейта вызван.
|
||||
|
||||
## AC-9 — F-2 не доигрывает Blocked/Needs Input (регресс)
|
||||
|
||||
- **Дано:** issue в Plane-статусе Blocked или Needs Input (не входит в
|
||||
{in_progress, approved, rejected}).
|
||||
- **Когда:** `reconcile_plane_once()`.
|
||||
- **Тогда:** ни `handle_status_start`, ни `handle_verdict` не вызываются для
|
||||
этого issue; `unblocked_total == 0`.
|
||||
- **PASS:** обработчики не вызваны.
|
||||
- **FAIL:** вызван любой обработчик.
|
||||
|
||||
## AC-10 — Never-raise: ошибка проверки статуса не ломает тик
|
||||
|
||||
- **Дано:** проверка blocked/needs-input (Plane API в Варианте A) бросает
|
||||
исключение для одной задачи; в выборке есть ещё одна валидная задача.
|
||||
- **Когда:** `reconcile_gate_once()`.
|
||||
- **Тогда:** тик не падает; сбойная задача консервативно НЕ трогается (skip);
|
||||
остальные обрабатываются.
|
||||
- **PASS:** исключение изолировано, остальные задачи обработаны.
|
||||
- **FAIL:** исключение всплыло из `reconcile_gate_once`.
|
||||
|
||||
## AC-11 — Лимит не хардкодится
|
||||
|
||||
- **Дано:** код F-1-гарда.
|
||||
- **Тогда:** используется `stage_engine.MAX_DEVELOPER_RETRIES`, а не литерал `3`.
|
||||
- **PASS:** граница берётся из константы.
|
||||
- **FAIL:** в reconciler.py появился магический `3`.
|
||||
|
||||
## AC-12 — Документация обновлена (golden source)
|
||||
|
||||
- **Дано:** PR задачи.
|
||||
- **Тогда:** обновлены `docs/architecture/README.md` (описание F-1 с новым skip),
|
||||
`CHANGELOG.md`, создан `06-adr/ADR-001-*.md`.
|
||||
- **PASS:** все три артефакта обновлены/созданы в этом же PR.
|
||||
- **FAIL:** любой отсутствует (reviewer → REQUEST_CHANGES).
|
||||
|
||||
## AC-13 — Регресс существующих тестов reconciler
|
||||
|
||||
- **Дано:** существующий `tests/test_reconciler.py` (ORCH-053).
|
||||
- **Когда:** `pytest tests/test_reconciler.py -q`.
|
||||
- **Тогда:** все прежние тесты зелёные (поведение happy-path/analysis/kill-switch
|
||||
не сломано).
|
||||
- **PASS:** 0 регрессий.
|
||||
- **FAIL:** любой ранее зелёный тест упал.
|
||||
82
docs/work-items/ORCH-060/04-test-plan.yaml
Normal file
82
docs/work-items/ORCH-060/04-test-plan.yaml
Normal file
@@ -0,0 +1,82 @@
|
||||
work_item: ORCH-060
|
||||
description: >
|
||||
Reconciler F-1 пропускает escalated (retry >= MAX_DEVELOPER_RETRIES) и
|
||||
явно-blocked / needs-input задачи; happy-path и no-spam сохранены.
|
||||
Конвенции test-фикстур — как в существующем tests/test_reconciler.py
|
||||
(изолированная sqlite-БД, моки Plane/Telegram/gate). Хелпер _make_task
|
||||
вставляет задачу; developer-ретраи моделируются вставкой N строк в agent_runs
|
||||
(agent='developer'); зелёный CI — через _green_ci(monkeypatch).
|
||||
|
||||
tests:
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "AC-1: escalated dev-задача (ровно MAX_DEVELOPER_RETRIES developer-ранов) при зелёном CI НЕ разблокируется — стадия остаётся development, unblocked_total==0, новых job нет"
|
||||
module: tests/test_reconciler.py
|
||||
setup: "_make_task('development', age_s=grace+60); insert MAX_DEVELOPER_RETRIES rows agent_runs(agent='developer'); _green_ci()"
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: "AC-2: developer-ранов > MAX_DEVELOPER_RETRIES (4–5) → также skip"
|
||||
module: tests/test_reconciler.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: "AC-3 (регресс happy-path): developer-ранов < MAX (0/1/2) при зелёном CI → задача доигрывается development->review, unblocked_total==1"
|
||||
module: tests/test_reconciler.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: "AC-4: граница — задача с ровно MAX пропущена, задача с MAX-1 доиграна (ровно одна advance)"
|
||||
module: tests/test_reconciler.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: "AC-5: задача в Plane-статусе Blocked (retry<лимита) пропущена — стадия не меняется, unblocked_total==0 (мок проверки статуса возвращает Blocked)"
|
||||
module: tests/test_reconciler.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: "AC-6: задача в Plane-статусе Needs Input (retry<лимита) пропущена"
|
||||
module: tests/test_reconciler.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: "AC-7 (no spam): на escalated-задаче не вызваны _note_unblock / send_telegram / notify_qg_failure; нет лог-строки 'разблокирована'"
|
||||
module: tests/test_reconciler.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: "AC-8: на escalated-задаче мок check_ci_green НЕ вызван (skip раньше пред-оценки гейта)"
|
||||
module: tests/test_reconciler.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: "AC-9 (регресс F-2): issue в Blocked/Needs Input не передаётся ни в handle_status_start, ни в handle_verdict при reconcile_plane_once; unblocked_total==0"
|
||||
module: tests/test_reconciler.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: "AC-10 (never-raise): проверка blocked/needs-input бросает исключение на одной задаче → тик не падает, сбойная skip, валидная соседняя обработана"
|
||||
module: tests/test_reconciler.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: "AC-11: граница берётся из stage_engine.MAX_DEVELOPER_RETRIES — тест с monkeypatch значения константы меняет точку отсечения (нет хардкода 3)"
|
||||
module: tests/test_reconciler.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: integration
|
||||
description: "AC-13 (регресс): полный прогон tests/test_reconciler.py (ORCH-053 кейсы) — все прежние тесты зелёные"
|
||||
module: tests/test_reconciler.py
|
||||
expected: PASS
|
||||
@@ -0,0 +1,161 @@
|
||||
# ADR-001: Reconciler (F-1) пропускает escalated / Blocked / Needs-Input задачи
|
||||
|
||||
- **Статус:** Accepted
|
||||
- **Дата:** 2026-06-07
|
||||
- **Задача:** ORCH-060
|
||||
- **Стадия:** architecture
|
||||
- **Связано:** adr-0007 (reconciler, ORCH-053) — уточняет контракт F-1;
|
||||
ORCH-046 (retry-счётчик), ORCH-047 (BLOCKED-вердикт)
|
||||
|
||||
## Контекст
|
||||
|
||||
ORCH-053 ввёл F-1 (`Reconciler._reconcile_gate_task`): для каждой не-терминальной
|
||||
задачи без активного job и старше grace делается read-only пред-оценка
|
||||
канонического QG; зелёный → `advance_if_gate_passed` →
|
||||
`advance_stage(..., finished_agent=None)`.
|
||||
|
||||
**Дефект (инцидент ET-013, 06–07.06.2026).** Задача, исчерпавшая лимит
|
||||
developer-ретраев (`_developer_retry_count(task_id) >= MAX_DEVELOPER_RETRIES = 3`),
|
||||
**escalated** в обработчиках `gitea.py` (`:280` CI-failure, `:371` review
|
||||
REQUEST_CHANGES) выполняет ТОЛЬКО `notify_error(...)`:
|
||||
|
||||
- стадия НЕ меняется (остаётся `development`);
|
||||
- терминального маркера в БД нет (нет столбца статуса в `tasks`);
|
||||
- активного job нет.
|
||||
|
||||
Для F-1 такая задача **неотличима** от «застрявшей из-за потерянного webhook».
|
||||
Если CI зелёный (типовой кейс: dev починил CI, но reviewer слал REQUEST_CHANGES
|
||||
до лимита), каждые `reconcile_interval_s` (120с) F-1 видит зелёный `check_ci_green`
|
||||
и разблокирует `development → review` → reviewer снова REQUEST_CHANGES → откат →
|
||||
снова эскалация (стадия не меняется) → следующий тик снова разблокирует.
|
||||
**Бесконечный цикл:** ET-013 разблокирована 10 раз за ночь, лишние запуски агентов
|
||||
(токены/деньги), спам в Telegram, паразитная нагрузка общего self-hosting-инстанса.
|
||||
|
||||
Симметричный риск: задачу, которую человек явно перевёл в Plane-статус **Blocked**
|
||||
/ **Needs Input** (ручной гейт), sweeper не должен авторазблокировать до
|
||||
вмешательства человека.
|
||||
|
||||
## Решение
|
||||
|
||||
В `_reconcile_gate_task` ПОСЛЕ существующих гардов (`stage=='analysis'` carve-out,
|
||||
`get_qg_for_stage is None`, `has_active_job_for_task`, grace) и ДО
|
||||
`advance_if_gate_passed` добавляются два пред-гарда. Любой срабатывает → ранний
|
||||
`return`: задача пропущена, гейт НЕ оценивается, `unblocked_total` не растёт,
|
||||
нотификаций нет.
|
||||
|
||||
### Гард 1 — escalated по ретраям (детерминированный, без сети) — **обязателен**
|
||||
|
||||
```python
|
||||
# ORCH-060: escalated tasks (max developer retries reached) are terminal —
|
||||
# they wait for a human, not the sweeper. Deterministic, no network.
|
||||
if developer_retry_count(task_id) >= MAX_DEVELOPER_RETRIES:
|
||||
return
|
||||
```
|
||||
|
||||
- Источник истины по retry — `agent_runs` (как у `_developer_retry_count`):
|
||||
`SELECT COUNT(*) FROM agent_runs WHERE task_id=? AND agent='developer'`.
|
||||
- `MAX_DEVELOPER_RETRIES` импортируется из `stage_engine` — **не хардкодить `3`**
|
||||
(AC-11).
|
||||
- Граница `>=` (на лимите — skip, на `лимит−1` — advance; AC-4).
|
||||
|
||||
**Промоут хелпера.** `stage_engine._developer_retry_count` повышается до публичного
|
||||
`developer_retry_count` (приватное имя сохраняется как алиас для существующих
|
||||
внутренних call-sites). Reconciler импортирует
|
||||
`MAX_DEVELOPER_RETRIES, developer_retry_count` из `stage_engine`. SQL **не
|
||||
дублируется** в `db.py` — единый источник истины по подсчёту ретраев.
|
||||
|
||||
### Гард 2 — явный человеческий Plane-статус (Blocked / Needs Input) — **Вариант A**
|
||||
|
||||
```python
|
||||
# ORCH-060: respect an explicit human gate (Blocked / Needs Input).
|
||||
if self._is_blocked_or_needs_input(task):
|
||||
return
|
||||
```
|
||||
|
||||
Механика — **Вариант A (запрос Plane API, без миграции схемы):**
|
||||
|
||||
1. Новый never-raise хелпер `plane_sync.fetch_issue_state(issue_id, project_id)
|
||||
-> str | None` — GET issue-detail (тот же endpoint/headers, что
|
||||
`fetch_issue_sequence_id` / `fetch_issue_fields`), возвращает uuid текущего
|
||||
`state`; любая ошибка/отсутствие поля → `None`.
|
||||
2. `Reconciler._is_blocked_or_needs_input(task)`:
|
||||
- `repo → ProjectConfig` через `projects.get_project_by_repo(task['repo'])`;
|
||||
- `pid = proj.plane_project_id`; `states = get_project_states(pid)` (кэш per-project);
|
||||
- `cur = fetch_issue_state(task['plane_id' | 'plane_issue_id'], pid)`;
|
||||
- вернуть `cur in {states['blocked'], states['needs_input']}`.
|
||||
- **Never-raise → консервативный фоллбэк:** любая ошибка/`None`/нерезолвленный
|
||||
проект → трактуем как «возможно заблокировано» → возвращаем `True` (skip).
|
||||
Не-разблокировать безопаснее, чем разблокировать (AC-10).
|
||||
|
||||
**Порядок гардов:** Гард 1 (локальный SQL, дёшево) — ПЕРВЫМ; Гард 2 (сеть) —
|
||||
вторым. Для зафиксированного инцидента (ET-013 = escalated) Гард 1 закрывает кейс
|
||||
**без единого сетевого вызова**.
|
||||
|
||||
### Что НЕ меняется (инварианты ORCH-053)
|
||||
|
||||
- Схема БД — **без миграции** (Вариант A). `STAGE_TRANSITIONS` / `QG_CHECKS` —
|
||||
без изменений. Гард — ВНЕ гейта: решает, ЗАПУСКАТЬ ли пред-оценку, а не меняет
|
||||
вердикт.
|
||||
- Never-raise на единицу работы (`reconcile_gate_once` per-task `try/except`
|
||||
сохраняется; новая логика не бросает наружу).
|
||||
- `analysis` carve-out, kill-switch'и (`reconcile_enabled`,
|
||||
`reconcile_plane_enabled`) — как прежде.
|
||||
- F-2 по существу не меняется: Blocked/Needs Input не входят в
|
||||
`{in_progress, approved, rejected}` → не доигрываются (фиксируется
|
||||
регресс-тестом AC-9).
|
||||
|
||||
### Опционально (вне scope AC, рекомендации)
|
||||
|
||||
- Под-флаг `reconcile_skip_blocked_enabled` (default `true`) для независимого
|
||||
отключения только Гарда 2 (сетевого), по аналогии с `reconcile_plane_enabled`.
|
||||
Гард 1 (локальный, безопасный) — всегда активен.
|
||||
- Best-effort счётчик `skipped_escalated` в снимке `GET /queue` (наблюдаемость).
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **Вариант B — локальный терминальный маркер в БД** (`tasks.blocked` /
|
||||
`tasks.reconcile_skip`, идемпотентный ALTER, выставляется в `set_issue_blocked`
|
||||
/ `set_issue_needs_input` и точках эскалации `gitea.py`). **Отклонён как
|
||||
primary:**
|
||||
- нарушает инвариант ORCH-053 «схема reconciler не меняется» (миграция на живой
|
||||
прод-БД = self-hosting-риск);
|
||||
- затрагивает больше точек записи (4+: две эскалации gitea + два set_issue_*) —
|
||||
выше риск рассинхрона маркера и факта;
|
||||
- для зафиксированного инцидента **не нужен**: Гард 1 (retry-count) закрывает
|
||||
ET-013 детерминированно и без сети.
|
||||
Вариант B остаётся задокументированным будущим упрочнением, если Plane-coupling
|
||||
Гарда 2 окажется болезненным (см. Последствия).
|
||||
- **Подавление в самом `advance_stage` / новый терминальный вердикт гейта** —
|
||||
отклонён: меняет общий критический путь; ORCH-053 уже постановил «не вызывать
|
||||
advance на красном», тот же принцип «не вызывать advance на escalated».
|
||||
- **Гард только по retry (без Гарда 2)** — недостаточно: не покрывает ручной
|
||||
Blocked при retry<лимита; AC-5/AC-6 требуют пропуск.
|
||||
|
||||
## Последствия
|
||||
|
||||
- **Плюсы:** ET-013-петля устранена детерминированно; 0 фантомных разблокировок,
|
||||
0 лишних запусков агентов, 0 спама по escalated-задачам; ручной Blocked/Needs
|
||||
Input уважается; без миграции БД и без изменения реестров → минимальный
|
||||
self-hosting-риск; единый источник истины по retry (промоут хелпера).
|
||||
- **Минусы / плата:**
|
||||
- Гард 2 вводит **per-candidate сетевой вызов** Plane на тике. Митигировано:
|
||||
кандидатов после grace+no-active-job немного; `get_project_states` кэшируется;
|
||||
Гард 1 отсекает escalated до сети.
|
||||
- **Plane-coupling F-1:** при недоступности Plane Гард 2 фоллбэкает в skip →
|
||||
F-1 во время Plane-outage не доигрывает кандидатов с retry<лимита (консерва-
|
||||
тивно «не навреди»). Приемлемо: outage редок/транзиентен; escalated-кейс
|
||||
(Гард 1) от Plane не зависит и продолжает работать; альтернатива
|
||||
(proceed-on-error) рискует вернуть bounce при реальном Blocked. Под-флаг
|
||||
`reconcile_skip_blocked_enabled` даёт ручной обход на время инцидента.
|
||||
- **Self-hosting:** изменение — чистая логика sweeper'а; прод-контейнер не
|
||||
рестартится/не роняется; деплой через staging (8501) по канону.
|
||||
|
||||
## Связи
|
||||
|
||||
- **adr-0007 (reconciler, ORCH-053)** — данный ADR уточняет контракт F-1
|
||||
(`_reconcile_gate_task` приобретает два пред-гарда; инварианты сохранены).
|
||||
- **adr-0003 (условный staging-гейт)** — образец never-raise + флага раската
|
||||
(Гард 2 / `reconcile_skip_blocked_enabled`).
|
||||
- **adr-0001 (реестр проектов)** — `get_project_by_repo` → `plane_project_id`
|
||||
для резолва per-project статусов (Вариант A).
|
||||
- ORCH-046 (retry-счётчик `agent_runs`), ORCH-047 (BLOCKED-вердикт).
|
||||
20
docs/work-items/ORCH-060/10-tech-risks.md
Normal file
20
docs/work-items/ORCH-060/10-tech-risks.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Технические риски: ORCH-060
|
||||
|
||||
Work Item ID: ORCH-060
|
||||
Стадия: architecture
|
||||
|
||||
| # | Риск | Вероятность | Влияние | Митигация |
|
||||
|---|------|-------------|---------|-----------|
|
||||
| R-1 | **Plane-coupling F-1.** Гард 2 (Вариант A) делает сетевой вызов на тике; при недоступности Plane все кандидаты с retry<лимита фоллбэкают в skip → F-1 временно не доигрывает. | Низкая (outage редок) | Среднее | Консервативный фоллбэк («не навреди»); escalated-кейс закрыт Гардом 1 без сети; под-флаг `reconcile_skip_blocked_enabled` для ручного обхода; `get_project_states` кэшируется. |
|
||||
| R-2 | **Стоимость поллинга.** Per-candidate GET issue-detail каждые 120с при большом числе stuck-задач. | Низкая | Низкое | Кандидатов после grace+no-active-job мало; Гард 1 (локальный SQL) отсекает escalated до сети; вызов только для переживших Гард 1. |
|
||||
| R-3 | **Промоут хелпера ломает call-sites.** `_developer_retry_count → developer_retry_count`. | Низкая | Среднее | Сохранить приватный алиас `_developer_retry_count = developer_retry_count`; grep всех вызовов перед мержем; покрыто существующими тестами stage_engine. |
|
||||
| R-4 | **Неверный фоллбэк-знак Гарда 2.** Если ошибку трактовать как «не заблокировано» → возврат ET-013-bounce при реальном Blocked. | Средняя (ошибка реализации) | Высокое | ADR явно фиксирует: ошибка/None/нерезолвленный проект → `True` (skip); AC-10 проверяет never-raise+skip. |
|
||||
| R-5 | **Резолв plane-issue-id из task.** В `tasks` два поля (`plane_id` / `plane_issue_id`); неверный выбор → пустой запрос. | Низкая | Низкое | Использовать тот же приоритет, что `get_task_by_plane_id` (оба поля); пустой id → фоллбэк skip. |
|
||||
| R-6 | **Регресс happy-path.** Слишком широкий гард пропустит честно-застрявшие задачи (retry<лимита, не Blocked). | Низкая | Высокое | AC-3/AC-4 (граница ровно на лимите); регресс существующих тестов AC-13. |
|
||||
| R-7 | **Self-hosting деплой.** Изменение работающего в проде sweeper'а. | Низкая | Высокое | Чистая логика, без миграции/рестарт-контрактов; обязательный прогон через staging (8501) перед прод-деплоем; kill-switch `reconcile_enabled`. |
|
||||
|
||||
## Вывод
|
||||
Все риски — низкие/средние по вероятности и митигируемы в рамках выбранной
|
||||
архитектуры (Вариант A, без миграции). Критичен корректный знак never-raise
|
||||
фоллбэка Гарда 2 (R-4) — выделен в AC-10. Схема БД и реестры не меняются →
|
||||
self-hosting-риск минимален.
|
||||
63
docs/work-items/ORCH-060/12-review.md
Normal file
63
docs/work-items/ORCH-060/12-review.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-060
|
||||
verdict: APPROVED
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-060
|
||||
|
||||
## Summary
|
||||
Reviewer-проверка PR `feature/ORCH-060-reconciler-escalated-max-retri` (commit `4db8276`,
|
||||
`fix(reconciler): skip escalated / Blocked / Needs-Input tasks in F-1`).
|
||||
|
||||
Задача — устранить инцидент ET-013 (бесконечная разблокировка escalated-задачи F-1-реконсайлером).
|
||||
Реализованы два пред-гарда в `Reconciler._reconcile_gate_task` строго ПОСЛЕ существующих гардов
|
||||
(`analysis` carve-out → нет гейта → активный job → grace) и ДО `advance_if_gate_passed`:
|
||||
- **Guard 1** (детерминированный, без сети, проверяется первым): `developer_retry_count(task_id) >= MAX_DEVELOPER_RETRIES`;
|
||||
- **Guard 2** (Вариант A — Plane API, never-raise → консервативный skip): `_is_blocked_or_needs_input(task)`.
|
||||
|
||||
Реализация **полностью соответствует** ТЗ (`02-trz.md`), критериям приёмки (`03-acceptance-criteria.md`)
|
||||
и ADR-001. Все 13 AC покрыты тестами (TC-01…TC-11 + sub-flag + F-2-регресс). `pytest tests/ -q` —
|
||||
**644 passed, 0 регрессий**; `tests/test_reconciler.py` — 27 passed.
|
||||
|
||||
## Соответствие ТЗ / ADR
|
||||
- **Guard 1** — точка вставки, граница `>=`, источник счётчика (`agent_runs`) совпадают с ТЗ §9 и ADR §«Гард 1». ✓
|
||||
- Промоут `stage_engine._developer_retry_count` → публичный `developer_retry_count`, приватный алиас сохранён, все 4 внутренних call-site (`stage_engine.py:565/613/874/950`) работают через алиас — единый источник истины, SQL не дублируется. ✓
|
||||
- `MAX_DEVELOPER_RETRIES` импортируется из `stage_engine`, **хардкода `3` в `reconciler.py` нет** (grep подтверждает). ✓ (AC-11)
|
||||
- **Guard 2 — Вариант A** без миграции БД: новый never-raise `plane_sync.fetch_issue_state` (тот же endpoint/headers, что `fetch_issue_sequence_id`), консервативный фоллбэк (`True`→skip) при любой ошибке/`None`/нерезолвленном проекте. Соответствует ADR §«Гард 2» и обоснованию выбора A над B. ✓
|
||||
- Под-флаг `reconcile_skip_blocked_enabled` (default `true`) гасит ТОЛЬКО сетевой Guard 2; Guard 1 всегда активен. ✓
|
||||
- Инварианты ORCH-053 сохранены: схема БД / `STAGE_TRANSITIONS` / `QG_CHECKS` не тронуты; never-raise на единицу работы (`reconcile_gate_once` per-task `try/except` + `_is_blocked_or_needs_input` внутренний `try/except`); тишина при пропуске (ранний `return` до `advance`, без `unblocked_total++`/лога/Telegram); `analysis` carve-out и kill-switch'и не изменены. ✓
|
||||
- API не изменён (`GET /queue` без изменений по содержимому) — соответствует ТЗ §4. ✓
|
||||
|
||||
## Качество кода
|
||||
- Docstrings на новых публичных/значимых функциях (`fetch_issue_state`, `developer_retry_count`, `_is_blocked_or_needs_input`) — содержательные, объясняют контракт never-raise и мотивацию. ✓
|
||||
- Обработка Plane-формата `state` (bare uuid и `{"id": ...}`-вложение) — defensive. ✓
|
||||
- Тесты содержательные (не тривиальные): граница ровно на лимите (TC-04), изоляция исключения с проверкой соседа (TC-10), отсутствие сетевого вызова гейта на escalated (TC-08), регресс F-2 (TC-09). ✓
|
||||
- Self-hosting: чистая логика sweeper'а, прод-контейнер не рестартится/не роняется. ✓
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- нет
|
||||
|
||||
### P1 — Must fix
|
||||
- нет
|
||||
|
||||
### P2 — Should fix
|
||||
- нет
|
||||
|
||||
> Замечание (P3 / информационно, не блокирует): Guard 2 делает per-candidate сетевой вызов Plane
|
||||
> для ВСЕХ репо (включая не-self-hosting), а не только для `orchestrator`. Это осознанное решение
|
||||
> Варианта A, явно зафиксировано в ADR §«Последствия» (митигировано: кандидатов после grace мало,
|
||||
> `get_project_states` кэшируется, Guard 1 отсекает escalated до сети). Соответствует ADR — не finding.
|
||||
|
||||
## Документация
|
||||
Обновлено в этом же PR (AC-12 — PASS):
|
||||
- `docs/work-items/ORCH-060/06-adr/ADR-001-reconciler-skip-escalated.md` — создан, Accepted, полное обоснование A vs B. ✓
|
||||
- `docs/architecture/README.md` — описание F-1 дополнено skip escalated/Blocked/Needs-Input; footer ORCH-060 переведён в статус «реализовано» с деталями. ✓
|
||||
- `CHANGELOG.md` — запись в `### Fixed` (`fix(reconciler): ...`). ✓
|
||||
- `README.md` — таблица env дополнена `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`. ✓
|
||||
- `.env.example` — канонический ключ + дескриптор добавлены (правило CLAUDE.md №8). ✓
|
||||
|
||||
Документация = golden source: код и доку обновлены синхронно. Нарушений нет.
|
||||
72
docs/work-items/ORCH-060/13-test-report.md
Normal file
72
docs/work-items/ORCH-060/13-test-report.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-060
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-060
|
||||
|
||||
Reconciler F-1 пропускает escalated (retry ≥ MAX_DEVELOPER_RETRIES) и явно
|
||||
Blocked / Needs-Input задачи; happy-path и no-spam сохранены.
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3 (plugins: anyio-4.13.0, asyncio-0.23.8)
|
||||
- Ветка: `feature/ORCH-060-reconciler-escalated-max-retri` @ `55e5e96`
|
||||
(фикс: `4db8276 fix(reconciler): skip escalated / Blocked / Needs-Input tasks in F-1`)
|
||||
- Дата: 2026-06-07
|
||||
- Review verdict: APPROVED (`12-review.md`)
|
||||
|
||||
## Smoke test API (прод 8500, read-only)
|
||||
> `curl` отсутствует в окружении тестера — проверка выполнена через `python urllib`.
|
||||
> Прод-контейнер НЕ перезапускался / не ронялся (self-hosting, CLAUDE.md §⚠️).
|
||||
|
||||
| Endpoint | HTTP | Ответ |
|
||||
|----------|------|-------|
|
||||
| `GET /health` | 200 | `{"status":"ok","service":"orchestrator"}` |
|
||||
| `GET /status` | 200 | активные задачи отданы (в т.ч. ORCH-060 stage=testing) |
|
||||
| `GET /queue` | 200 | counts/resilience/reconcile-блок отданы |
|
||||
|
||||
## Результаты (test-plan 04-test-plan.yaml → AC)
|
||||
|
||||
| TC ID | AC | Описание | Тест | Результат |
|
||||
|-------|-----|----------|------|-----------|
|
||||
| TC-01 | AC-1 | escalated == MAX_DEVELOPER_RETRIES при зелёном CI → skip | `test_tc060_01_escalated_at_limit_skipped` | PASS |
|
||||
| TC-02 | AC-2 | dev-ранов > MAX → skip | `test_tc060_02_over_limit_skipped` | PASS |
|
||||
| TC-03 | AC-3 | регресс happy-path: retry < MAX → advance dev→review | `test_tc060_03_under_limit_still_advances` | PASS |
|
||||
| TC-04 | AC-4 | граница: ровно MAX skip, MAX−1 advance (ровно одна) | `test_tc060_04_boundary_exactly_one_advances` | PASS |
|
||||
| TC-05 | AC-5 | Plane-статус Blocked → skip | `test_tc060_05_blocked_skipped` | PASS |
|
||||
| TC-06 | AC-6 | Plane-статус Needs Input → skip | `test_tc060_06_needs_input_skipped` | PASS |
|
||||
| TC-07 | AC-7 | no spam на escalated (нет _note_unblock/telegram/qg-fail) | `test_tc060_07_escalated_no_spam` | PASS |
|
||||
| TC-08 | AC-8 | escalated → мок check_ci_green НЕ вызван (skip раньше гейта) | `test_tc060_08_no_gate_call_on_escalated` | PASS |
|
||||
| TC-09 | AC-9 | регресс F-2: Blocked/Needs Input не доигрывается | `test_tc060_09_f2_does_not_replay_blocked` | PASS |
|
||||
| TC-10 | AC-10 | never-raise: ошибка guard2 изолирована, сосед обработан | `test_tc060_10_guard2_never_raise` | PASS |
|
||||
| TC-11 | AC-11 | граница из stage_engine.MAX_DEVELOPER_RETRIES (нет хардкода 3) | `test_tc060_11_limit_from_constant` | PASS |
|
||||
| — | — | под-флаг `reconcile_skip_blocked_enabled` гасит только guard2 | `test_tc060_subflag_disables_only_guard2` | PASS |
|
||||
| TC-12 | AC-13 | регресс: полный прогон test_reconciler.py (ORCH-053 кейсы) | `tests/test_reconciler.py` (27 passed) | PASS |
|
||||
| — | AC-12 | документация (README/ADR/CHANGELOG) — проверено reviewer'ом | — | PASS |
|
||||
|
||||
## Вывод pytest
|
||||
|
||||
Полный регресс:
|
||||
```
|
||||
$ python -m pytest tests/ -q
|
||||
........................................................................ [ 11%]
|
||||
... (644 dots) ...
|
||||
.................................................................... [100%]
|
||||
644 passed, 1 warning in 15.65s
|
||||
```
|
||||
|
||||
Целевой модуль:
|
||||
```
|
||||
$ python -m pytest tests/test_reconciler.py -v
|
||||
...
|
||||
27 passed, 1 warning in 1.23s
|
||||
```
|
||||
(1 warning — PydanticDeprecatedSince20 в `src/config.py:4`, не связано с ORCH-060,
|
||||
существующий технический долг.)
|
||||
|
||||
## Итог
|
||||
**PASS** — все 13 критериев приёмки покрыты и зелёные, полный регресс 644/644,
|
||||
целевой модуль 27/27, smoke API 3/3. Регрессий нет. Задача готова к стадии
|
||||
deploy-staging.
|
||||
80
docs/work-items/ORCH-060/15-staging-log.md
Normal file
80
docs/work-items/ORCH-060/15-staging-log.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
staging_status: FAILED
|
||||
timestamp: 2026-06-07T11:57:34Z
|
||||
base_url: http://localhost:8501
|
||||
mode: stub
|
||||
result: 8/10
|
||||
work_item: ORCH-060
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite **FAILED** (exit code 1, 8/10 checks PASS).
|
||||
|
||||
Canonical run (ORCH-048, ADR-001) — executed INSIDE the `orchestrator-staging`
|
||||
container against the live staging instance:
|
||||
|
||||
```
|
||||
python3 /repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub
|
||||
```
|
||||
|
||||
## Failing checks
|
||||
|
||||
- **C9a — Branch appears in `orchestrator-sandbox`** → FAIL (`branch=not found`).
|
||||
After triggering the pipeline via `POST /webhook/plane`, no feature branch was
|
||||
created in the sandbox repo within the 60s poll window.
|
||||
- **C9b — Analyst job enqueued in staging queue** → FAIL. No analyst job appeared
|
||||
in the staging job queue within the 30s window.
|
||||
|
||||
Both failures are in the E2E block (Block C): the webhook was accepted
|
||||
(C8 → HTTP 200 `{'status': 'accepted'}`) and the Plane issue was created (C7 →
|
||||
HTTP 201), but the pipeline did not materialise a branch or enqueue the analyst
|
||||
job — the staging instance did not actually process the triggered task end-to-end.
|
||||
|
||||
## Passing checks (8/10)
|
||||
|
||||
- Block A (SMOKE): A1 /health 200, A2 /queue shape, A3 ORCH_STAGING=true.
|
||||
- Block B (ACCESS): B4 Plane sandbox reachable, B5 Gitea sandbox push=true,
|
||||
B6 registry isolation (sandbox present, prod ET/ORCH absent — confirms the
|
||||
canonical in-container run; B6 would false-FAIL from the host).
|
||||
|
||||
## Verdict
|
||||
|
||||
Machine verdict is authoritative: exit code 1 → `staging_status: FAILED`.
|
||||
Per the conditional staging gate (ORCH-35), a FAILED staging gate for the
|
||||
self-hosting repo rolls the task back to `development`.
|
||||
|
||||
## Raw output
|
||||
|
||||
```
|
||||
============================================================
|
||||
ORCH-33 Staging Check Suite
|
||||
base_url : http://localhost:8501
|
||||
mode : stub
|
||||
utc_time : 2026-06-07T11:55:50.247315+00:00
|
||||
============================================================
|
||||
|
||||
[Block A] SMOKE
|
||||
✓ PASS A1 GET /health → 200 status=ok [HTTP 200, body={'status': 'ok', 'service': 'orchestrator'}]
|
||||
✓ PASS A2 GET /queue → 200 with counts/max_concurrency/resilience [HTTP 200, keys=['counts', 'max_concurrency', 'poll_interval', 'resilience', 'reconcile', 'recent']]
|
||||
✓ PASS A3 ORCH_STAGING=true (not prod) [ORCH_STAGING=true]
|
||||
|
||||
[Block B] ACCESS
|
||||
✓ PASS B4 Plane: sandbox project accessible [HTTP 200, found 5 project(s), sandbox=YES]
|
||||
✓ PASS B5 Gitea: orchestrator-sandbox accessible, push=true [HTTP 200, permissions={'admin': True, 'push': True, 'pull': True}]
|
||||
✓ PASS B6 Registry: sandbox present, prod ET/ORCH absent [sandbox=YES, prod-ET=NO(good), prod-ORCH=NO(good)]
|
||||
|
||||
[Block C] E2E (mode=stub)
|
||||
C7 Create issue in Plane SANDBOX [HTTP 201, issue_id=a05995d1-4e3c-44f7-af6f-8bd28fa6367d]
|
||||
C8 Trigger pipeline via /webhook/plane [HTTP 200, resp={'status': 'accepted'}]
|
||||
✗ FAIL C9a Branch appears in orchestrator-sandbox [branch=not found]
|
||||
✗ FAIL C9b Analyst job enqueued in staging queue
|
||||
|
||||
[CLEANUP]
|
||||
✓ PASS CLEANUP: deleted Plane issue a05995d1-4e3c-44f7-af6f-8bd28fa6367d (HTTP 204)
|
||||
|
||||
============================================================
|
||||
RESULT: 8/10 checks PASS
|
||||
============================================================
|
||||
__EXIT_CODE__=1
|
||||
```
|
||||
7
docs/work-items/ORCH-061/00-business-request.md
Normal file
7
docs/work-items/ORCH-061/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: BUG: deploy-staging петля — откат на development (self-deploy)
|
||||
|
||||
Work Item ID: ORCH-061
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
117
docs/work-items/ORCH-061/01-brd.md
Normal file
117
docs/work-items/ORCH-061/01-brd.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# 01 — BRD: BUG — deploy-staging петля (откат deploy-staging → development) для self-deploy
|
||||
|
||||
Work Item: **ORCH-061**
|
||||
Тип: **BUG**
|
||||
Приоритет: **P0**
|
||||
Репозиторий: `orchestrator` (self-hosting)
|
||||
Эпик-контекст: блокер **ORCH-54** (автономное внедрение self-hosting)
|
||||
|
||||
---
|
||||
|
||||
## 1. Резюме (Executive summary)
|
||||
|
||||
На стадии `deploy-staging` для self-hosting репозитория `orchestrator` задача
|
||||
зацикливается: гейт ребра `deploy-staging → deploy` даёт FAILED, `stage_engine`
|
||||
откатывает задачу `deploy-staging → development`, developer-агент перезапускается,
|
||||
проходит конвейер заново, снова упирается в `deploy-staging`, снова откат — и так
|
||||
по кругу (с расходом developer-ретраев и кредитов LLM), либо до исчерпания лимита
|
||||
ретраев и блокировки.
|
||||
|
||||
Следствие: **прод-деплой self-hosting репо невозможен автономно**. Последние
|
||||
ORCH-задачи (ORCH-58, ORCH-60) доводились до прода **вручную** (ручной merge PR +
|
||||
ручной build-once retag + ручной `--deploy`). Это прямой блокер автономного
|
||||
внедрения (эпик ORCH-54).
|
||||
|
||||
## 2. Бизнес-контекст и проблема
|
||||
|
||||
Оркестратор дорабатывает сам себя (self-hosting). Стадия `deploy-staging`
|
||||
(порт 8501) — обязательная страховка перед прод-деплоем орка (ORCH-35, ADR-0003).
|
||||
На этой стадии deployer гоняет `scripts/staging_check.py` против живого
|
||||
staging-стенда и пишет машинный вердикт `staging_status: SUCCESS|FAILED` в
|
||||
`15-staging-log.md`. Гейт `check_staging_status` читает этот вердикт; `FAILED`
|
||||
→ откат на `development` (ORCH-35, `_handle_qg_failure_rollbacks`).
|
||||
|
||||
Подтверждены **две независимые причины** зацикливания.
|
||||
|
||||
### Причина №1 — ложный FAILED `check_staging_status` (контекст ORCH-58)
|
||||
|
||||
`staging_check.py` в sandbox-прогоне даёт **8/10 PASS, 2 ложных FAIL** на e2e-блоке
|
||||
Block C:
|
||||
- **C9a** — ветка не появляется в `orchestrator-sandbox` (branch not found);
|
||||
- **C9b** — analyst-job не появляется в staging-очереди (`/queue → recent`).
|
||||
|
||||
Сопутствующая пометка suite: «Plane comment check skipped: bot-tokens not added to
|
||||
SANDBOX project» — bot-аккаунты агентов (`ORCH_PLANE_BOT_*`) не добавлены членами
|
||||
SANDBOX-проекта Plane (проект создан после провижининга ботов). Это **отсутствие
|
||||
sandbox-настроек инфраструктуры, а не регресс кода**. Тем не менее `staging_check.py`
|
||||
возвращает ненулевой exit-code → deployer пишет `staging_status: FAILED` → гейт
|
||||
FAILED → откат `deploy-staging → development`.
|
||||
|
||||
### Причина №2 — «no changes to commit» на action-стадии (контекст ORCH-60)
|
||||
|
||||
Стадии деплоя по своей природе **действие, а не правка кода** (рестарт/retag), и
|
||||
закономерно не порождают git-изменений в `src/`/`tests/`. Сигнал «no changes»
|
||||
для action-стадии не должен трактоваться как недовыполнение работы; критерий успеха
|
||||
action-стадии — успешное выполнение действия (exit0 + доказанный health/staging),
|
||||
а не наличие нового коммита. Сейчас отсутствие изменений на стадии деплоя приводит
|
||||
к недопродвижению задачи и откату.
|
||||
|
||||
### Совокупный эффект
|
||||
|
||||
Любая из причин по отдельности достаточна, чтобы зациклить self-deploy. Обе
|
||||
проявились на реальных задачах ORCH-58 и ORCH-60, которые пришлось доводить вручную.
|
||||
|
||||
## 3. Цели (Goals)
|
||||
|
||||
- **G1.** ORCH-задача для self-hosting `orchestrator` проходит
|
||||
`deploy-staging → deploy → done` **без ручного вмешательства** и **без петли**.
|
||||
- **G2.** Ложный (инфраструктурный) FAIL `staging_check` в sandbox **не вызывает**
|
||||
откат `deploy-staging → development`.
|
||||
- **G3.** Отсутствие git-изменений на стадиях деплоя (`deploy-staging` / `deploy`)
|
||||
**не трактуется** как недовыполнение и не приводит к откату.
|
||||
- **G4.** Реальный регресс (настоящий провал staging-проверки или прод-деплоя)
|
||||
**по-прежнему** приводит к откату `→ development` (страховка не ослабляется).
|
||||
|
||||
## 4. Вне области (Non-goals)
|
||||
|
||||
- Полная автоматизация ручного approve прод-деплоя (это ORCH-54).
|
||||
- Изменение конвейера стадий (`STAGE_TRANSITIONS`), реестра гейтов как структуры,
|
||||
контрактов `check_deploy_status` / `check_staging_status` frontmatter-вердиктов.
|
||||
- Изменение поведения для **не**-self-hosting репозиториев (enduro-trails и пр.):
|
||||
для них staging-гейт и self-deploy остаются no-op / прежними.
|
||||
- Изменение схемы БД.
|
||||
|
||||
## 5. Заинтересованные стороны
|
||||
|
||||
| Роль | Интерес |
|
||||
|------|---------|
|
||||
| Owner / оператор оркестратора | Автономный self-deploy без ручных шагов и без ночных петель. |
|
||||
| Другие проекты (enduro-trails) | Их конвейер не должен быть затронут (общий инстанс, общая очередь). |
|
||||
| Агенты (deployer) | Чёткий, не ложно-срабатывающий контракт стадии деплоя. |
|
||||
|
||||
## 6. Кандидатные направления решения (из бизнес-запроса)
|
||||
|
||||
Бизнес-запрос называет два направления (одно или оба); **выбор и механизм —
|
||||
за архитектором (ADR)**, BRD требует лишь достижения G1–G4:
|
||||
|
||||
- **(а)** Сделать sandbox-прогон `staging_check` честным (например, настроить
|
||||
bot-токены SANDBOX Plane-проекта / починить sandbox e2e), чтобы C9a/C9b
|
||||
проходили честно (10/10) и `check_staging_status` не падал ложно.
|
||||
- **(б)** Отвязать продвижение стадий деплоя от git-changes для self-deploy:
|
||||
успех action-стадии = exit0 + health/staging PASS, а не наличие коммита.
|
||||
|
||||
## 7. Бизнес-эффект / риски бездействия
|
||||
|
||||
- **Эффект:** разблокировка автономного внедрения self-hosting (ORCH-54);
|
||||
устранение ручного труда (merge + retag + deploy) и риска ошибки при ручных шагах.
|
||||
- **Риск бездействия:** каждая ORCH-задача требует ручного дотягивания до прода;
|
||||
петли жгут кредиты LLM и developer-ретраи, задачи блокируются.
|
||||
|
||||
## 8. Допущения
|
||||
|
||||
- Прод-контейнер `orchestrator` (8500) обслуживает все проекты из общего инстанса —
|
||||
его **нельзя** ронять/перезапускать в рамках задачи (см. CLAUDE.md, INFRA.md).
|
||||
- Изменения касаются self-hosting пути (`is_self_hosting_repo` / `self_deploy_applies`);
|
||||
для прочих репо поведение не меняется.
|
||||
- Документация — golden source: затронутые `docs/architecture/README.md`,
|
||||
`docs/operations/STAGING_CHECK.md`, `CHANGELOG.md` обновляются в том же PR.
|
||||
145
docs/work-items/ORCH-061/02-trz.md
Normal file
145
docs/work-items/ORCH-061/02-trz.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# 02 — ТЗ: устранение петли deploy-staging → development при self-deploy
|
||||
|
||||
Work Item: **ORCH-061** · Тип: **BUG** · Приоритет: **P0** · Репо: `orchestrator`
|
||||
|
||||
> Это ТЗ фиксирует **требования и контракты**, которые должна удовлетворить
|
||||
> реализация. Конкретный архитектурный механизм (направление (а), (б) или оба;
|
||||
> где именно разместить логику) выбирает архитектор в ADR (`06-adr/`).
|
||||
> ТЗ намеренно не предписывает дизайн, но задаёт инварианты и границы изменений.
|
||||
|
||||
---
|
||||
|
||||
## 1. Затронутые модули `src/` и артефакты
|
||||
|
||||
Прямо относящиеся к дефекту (для контекста; точечный набор правок — за архитектором):
|
||||
|
||||
| Файл | Роль в дефекте |
|
||||
|------|----------------|
|
||||
| `scripts/staging_check.py` | e2e-suite; C9a (branch) / C9b (analyst job) дают ложный FAIL в sandbox; exit-code управляет вердиктом deployer. |
|
||||
| `src/qg/checks.py` → `check_staging_status`, `_parse_staging_status` | гейт ребра `deploy-staging→deploy`; читает `staging_status:` из `15-staging-log.md`. |
|
||||
| `src/stage_engine.py` → `advance_stage`, `_handle_qg_failure_rollbacks` | откат `deploy-staging→development` при FAILED (ветка `agent=="deployer" and qg=="check_staging_status"`). |
|
||||
| `src/agents/launcher.py` → `_handle_completion`/`_try_advance_stage` | пост-ран git-commit; лог «no changes to commit»; обработка deployer-стадий. |
|
||||
| `src/self_deploy.py` | Phase A/B/C исполняемого self-deploy (контекст продвижения `deploy`). |
|
||||
| `src/config.py` | место для kill-switch/настроек нового поведения (если потребуется). |
|
||||
| `.openclaw/agents/deployer.md` | инструкция deployer о написании вердикта; обновить при смене контракта. |
|
||||
| `docs/operations/STAGING_CHECK.md`, `docs/architecture/README.md`, `CHANGELOG.md` | golden-source документация (обновить в том же PR). |
|
||||
|
||||
## 2. Функциональные требования
|
||||
|
||||
### FR-1 — Нет петли на корректном self-deploy
|
||||
Для self-hosting `orchestrator`, при корректном состоянии (реальный pipeline в
|
||||
порядке, staging-стенд здоров), задача проходит `deploy-staging → deploy → done`
|
||||
**без отката** `deploy-staging → development` и **без ручного вмешательства**.
|
||||
|
||||
### FR-2 — Ложный (инфраструктурный) FAIL не вызывает откат
|
||||
Ложное падение `staging_check` в sandbox, вызванное **исключительно** отсутствием
|
||||
sandbox-настроек (например, C9a/C9b при ненастроенных bot-токенах SANDBOX), не
|
||||
приводит к `staging_status: FAILED` → откату. Должно быть реализовано одним из
|
||||
способов (выбор — ADR):
|
||||
- **(а)** sandbox-инфраструктура приведена в состояние, при котором C9a/C9b
|
||||
проходят честно (10/10); и/или
|
||||
- **(б)** вердикт staging-гейта перестаёт зависеть от заведомо инфраструктурных
|
||||
(не пайплайновых) проверок — например, осознанный allowlist/threshold
|
||||
«известных sandbox-инфра» проверок, отделённый от реальных pipeline-проверок.
|
||||
|
||||
> Любой механизм по FR-2 **обязан** сохранить FR-4 (реальный провал ловится).
|
||||
|
||||
### FR-3 — «no changes» на action-стадии не есть недовыполнение
|
||||
На стадиях деплоя (`deploy-staging`, `deploy`) для self-deploy отсутствие
|
||||
git-изменений (`no changes to commit`) **не** трактуется как недовыполнение и
|
||||
**не** приводит к откату/блокировке. Критерий успеха action-стадии = успешный
|
||||
exit агента/хука + доказанный health/staging-вердикт, а **не** наличие нового
|
||||
коммита.
|
||||
|
||||
### FR-4 — Реальный регресс по-прежнему откатывается (страховка цела)
|
||||
- Настоящий провал реальных pipeline-проверок staging → `staging_status: FAILED`
|
||||
→ откат `deploy-staging → development` (как сейчас).
|
||||
- Настоящий провал прод-деплоя (`deploy_status: FAILED`, БАГ-8) → откат
|
||||
`deploy → development` (как сейчас).
|
||||
- Ослабления страховки быть не должно: «зелёный по умолчанию» при недоступности
|
||||
проверок запрещён (fail-closed для реальных проверок сохраняется).
|
||||
|
||||
### FR-5 — Условность self-hosting сохранена
|
||||
Изменения активны **только** для self-hosting пути
|
||||
(`is_self_hosting_repo` / `self_deploy_applies`). Для прочих репозиториев
|
||||
поведение `check_staging_status` (no-op N/A) и стадии деплоя — **без изменений**.
|
||||
|
||||
### FR-6 — Управляемость (kill-switch)
|
||||
Любое новое поведение (толерантность к инфра-FAIL и/или отвязка от git-changes)
|
||||
закрыто отдельным флагом конфигурации (по образцу `merge_gate_enabled`,
|
||||
`image_freshness_enabled`, `self_deploy_enabled`), с безопасным дефолтом и
|
||||
возможностью мгновенно вернуть прежнее поведение без передеплоя кода-логики.
|
||||
|
||||
### FR-7 — Наблюдаемость
|
||||
Срабатывание нового поведения (например, «staging_check: проигнорирован
|
||||
инфра-FAIL C9a/C9b» или «action-стадия: no-changes ожидаемо») логируется явной
|
||||
строкой и при необходимости отражается в Plane-комментарии/Telegram, чтобы
|
||||
оператор отличал «реальный зелёный» от «зелёного с допущением».
|
||||
|
||||
## 3. Изменения API
|
||||
|
||||
API эндпоинты (`/health`, `/status`, `/queue`, `/webhook/*`) — **без изменений**.
|
||||
Допускается расширение снапшота `GET /queue` диагностическим полем (опционально,
|
||||
по решению архитектора) — без удаления/переименования существующих ключей.
|
||||
|
||||
## 4. Изменения схемы БД
|
||||
|
||||
**Нет.** Схема (`events`, `tasks`, `agent_runs`, `jobs`) не меняется. Любое
|
||||
restart-safe состояние (если потребуется) — через существующие паттерны
|
||||
(sentinel-файлы / поля `jobs.task_content`), без миграций.
|
||||
|
||||
## 5. Контракты, которые НЕЛЬЗЯ менять
|
||||
|
||||
- `STAGE_TRANSITIONS` (порядок и состав стадий) и `get_previous_stage`.
|
||||
- Состав/семантика `QG_CHECKS` как реестра; frontmatter-контракты
|
||||
`staging_status:` (`15-staging-log.md`) и `deploy_status:` (`14-deploy-log.md`) —
|
||||
читаются ТОЛЬКО из YAML-frontmatter, значения `SUCCESS|FAILED`.
|
||||
- Откатные контракты БАГ-8 (`deploy→development`) и ORCH-35
|
||||
(`deploy-staging→development`) для **реальных** провалов.
|
||||
- Контракт exit-code хука деплоя (`0/1/2`) и `map_exit_code_to_status`.
|
||||
- Поведение для не-self-hosting репозиториев.
|
||||
|
||||
## 6. Требования к новым/изменённым QG checks
|
||||
|
||||
- Если выбран механизм толерантности (FR-2 вариант б), он реализуется **внутри**
|
||||
существующего пути `check_staging_status` / staging-вердикта (не новая стадия),
|
||||
по образцу условности ORCH-35; контракт «never-raise» сохраняется.
|
||||
- Любая новая проверка/под-чек регистрируется в `QG_CHECKS` и покрывается
|
||||
снапшот-тестом реестра (`tests/test_qg_registry_snapshot.py`).
|
||||
|
||||
## 7. Требования к staging_check.py (если затрагивается)
|
||||
|
||||
- Если выбран механизм классификации проверок (FR-2 вариант б через suite),
|
||||
e2e-проверки, заведомо зависящие от sandbox-инфраструктуры (C9a/C9b и связанные),
|
||||
должны быть **отличимы** (по метке/категории) от реальных pipeline-проверок,
|
||||
чтобы вердикт и/или exit-code мог их учитывать осознанно. Прежний дефолтный
|
||||
режим (`stub`/`full-real`) и существующие проверки A/B сохраняются.
|
||||
- Никакого «всегда 0»: реальный провал реальных проверок обязан давать ненулевой
|
||||
exit-code / FAIL-категорию.
|
||||
|
||||
## 8. Требования к pipeline-артефактам
|
||||
|
||||
- Стадия деплоя по-прежнему производит машинный вердикт-артефакт
|
||||
(`15-staging-log.md` / `14-deploy-log.md`) с корректным frontmatter.
|
||||
- Артефакты, обновляемые по pipeline в этом PR: `docs/architecture/README.md`
|
||||
(раздел про staging-гейт/self-deploy — отметить ORCH-061),
|
||||
`docs/operations/STAGING_CHECK.md` (поведение C9a/C9b и/или sandbox-настройка),
|
||||
`CHANGELOG.md`, при изменении контракта — `.openclaw/agents/deployer.md`.
|
||||
- ADR: `docs/work-items/ORCH-061/06-adr/ADR-001-*.md` (решение по направлению/механизму).
|
||||
|
||||
## 9. Нефункциональные требования
|
||||
|
||||
- **Безопасность self-hosting:** реализация НЕ перезапускает/не роняет прод 8500
|
||||
в рамках задачи; сборки/recreate — только staging (8501).
|
||||
- **Идемпотентность / restart-safe:** новое поведение переживает рестарт инстанса.
|
||||
- **never-raise:** дефект-исправляющая логика не должна пробрасывать исключения в
|
||||
`advance_stage` (по образцу merge-gate / image-freshness).
|
||||
- **Обратная совместимость:** при выключенном флаге (FR-6) — прежнее поведение 1:1.
|
||||
- **Тестируемость:** «чистая» вердикт-логика выделяется так, чтобы покрываться
|
||||
unit-тестами без live staging/docker.
|
||||
|
||||
## 10. Зависимости и связанные задачи
|
||||
|
||||
- ORCH-35 (условный staging-гейт, ADR-0003), ORCH-36 (исполняемый self-deploy,
|
||||
ADR-0007), ORCH-58 (провенанс staging-образа), ORCH-60 (skip escalated/Blocked).
|
||||
- Блокирует: ORCH-54 (автономное внедрение).
|
||||
90
docs/work-items/ORCH-061/03-acceptance-criteria.md
Normal file
90
docs/work-items/ORCH-061/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# 03 — Критерии приёмки: ORCH-061
|
||||
|
||||
Work Item: **ORCH-061** · Тип: **BUG** · Приоритет: **P0**
|
||||
|
||||
Формат: каждый критерий имеет чёткое условие **PASS/FAIL**. Критерии outcome-ориентированы
|
||||
(не предписывают механизм); реализация может удовлетворить FR-2 направлением (а), (б) или обоими.
|
||||
|
||||
---
|
||||
|
||||
## AC-1 — Автономный проход self-deploy без петли (главный критерий)
|
||||
- **PASS:** для self-hosting `orchestrator` задача в состоянии `deploy-staging`
|
||||
при здоровом стенде и корректном pipeline продвигается `deploy-staging → deploy`
|
||||
(далее по штатному approve → `done`) **без** отката на `development` и **без**
|
||||
ручного вмешательства в шаги staging/merge/retag/deploy.
|
||||
- **FAIL:** наблюдается хотя бы один автоматический откат `deploy-staging → development`
|
||||
при отсутствии реального регресса, либо для прохода требуется ручной шаг.
|
||||
|
||||
## AC-2 — Ложный инфраструктурный FAIL не откатывает
|
||||
- **PASS:** прогон, где **единственные** падения — заведомо sandbox-инфраструктурные
|
||||
(C9a branch-not-found / C9b analyst-job-not-in-queue при ненастроенных bot-токенах
|
||||
SANDBOX), а все реальные pipeline-проверки зелёные, приводит к
|
||||
`staging_status: SUCCESS` (или эквивалентному «не-FAILED») → **нет** отката.
|
||||
- **FAIL:** такой прогон даёт `staging_status: FAILED` → откат `deploy-staging → development`.
|
||||
|
||||
## AC-3 — Реальный провал staging по-прежнему откатывает (страховка цела)
|
||||
- **PASS:** прогон с провалом **реальной** pipeline-проверки (не инфра-исключение)
|
||||
даёт `staging_status: FAILED` → откат `deploy-staging → development` +
|
||||
`set_issue_blocked`/нотификации (как сейчас, ORCH-35).
|
||||
- **FAIL:** реальный провал staging проходит как успех / задача доходит до `deploy`.
|
||||
|
||||
## AC-4 — «no changes to commit» на action-стадии не есть недовыполнение
|
||||
- **PASS:** на стадиях `deploy-staging`/`deploy` для self-deploy отсутствие
|
||||
git-изменений не вызывает откат/блокировку; продвижение определяется успешным
|
||||
exit + health/staging-вердиктом.
|
||||
- **FAIL:** отсутствие коммита на стадии деплоя приводит к откату/недопродвижению.
|
||||
|
||||
## AC-5 — Реальный провал прод-деплоя по-прежнему откатывает (БАГ-8 цел)
|
||||
- **PASS:** `deploy_status: FAILED` (exit-code хука ≠ 0) → откат `deploy → development`
|
||||
+ `set_issue_blocked` + release merge-lease + clear deploy-state (как сейчас).
|
||||
- **FAIL:** провал прод-деплоя проходит как `done`.
|
||||
|
||||
## AC-6 — Условность self-hosting сохранена
|
||||
- **PASS:** для не-self-hosting репо (`is_self_hosting_repo == False`)
|
||||
`check_staging_status` остаётся `(True, "Staging gate N/A …")`, стадия деплоя
|
||||
работает как прежде; поведение этих репо байт-в-байт не изменилось.
|
||||
- **FAIL:** изменилось поведение для не-self-hosting репозиториев.
|
||||
|
||||
## AC-7 — Kill-switch возвращает прежнее поведение
|
||||
- **PASS:** при выключенном флаге нового поведения (FR-6) система ведёт себя 1:1
|
||||
как до ORCH-061 (включая прежний откат на инфра-FAIL, если флаг выключен).
|
||||
- **FAIL:** новое поведение невозможно отключить / выключение не восстанавливает старое.
|
||||
|
||||
## AC-8 — Контракты не сломаны
|
||||
- **PASS:** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, frontmatter-контракты
|
||||
`staging_status:`/`deploy_status:` (только YAML, `SUCCESS|FAILED`), exit-code хука
|
||||
(0/1/2) и `map_exit_code_to_status` — без регресса; снапшот-тест реестра гейтов зелёный.
|
||||
- **FAIL:** изменены контракты стадий/гейтов/вердиктов или сломан снапшот реестра.
|
||||
|
||||
## AC-9 — Схема БД не меняется
|
||||
- **PASS:** нет миграций; `events`/`tasks`/`agent_runs`/`jobs` без изменений схемы.
|
||||
- **FAIL:** добавлена/изменена колонка/таблица.
|
||||
|
||||
## AC-10 — never-raise
|
||||
- **PASS:** новая логика в пути `advance_stage`/staging-вердикта при любой внутренней
|
||||
ошибке (docker/ssh/io/парсинг) даёт безопасный детерминированный вердикт и не
|
||||
пробрасывает исключение в `advance_stage`.
|
||||
- **FAIL:** исключение из новой логики всплывает в `advance_stage`/останавливает конвейер.
|
||||
|
||||
## AC-11 — Наблюдаемость
|
||||
- **PASS:** срабатывание нового поведения (игнор инфра-FAIL / ожидаемые no-changes)
|
||||
даёт явную лог-строку (и при необходимости коммент/Telegram), позволяющую отличить
|
||||
«честно зелёный» от «зелёного с допущением».
|
||||
- **FAIL:** новое поведение срабатывает молча, неотличимо от честного зелёного.
|
||||
|
||||
## AC-12 — Безопасность self-hosting
|
||||
- **PASS:** реализация не перезапускает/не роняет прод-контейнер 8500 в рамках
|
||||
задачи; любые сборки/recreate — только staging (8501).
|
||||
- **FAIL:** код пути задачи рестартит/собирает прод 8500.
|
||||
|
||||
## AC-13 — Документация обновлена (golden source)
|
||||
- **PASS:** в том же PR обновлены `docs/architecture/README.md`,
|
||||
`docs/operations/STAGING_CHECK.md` (поведение C9a/C9b и/или sandbox-настройка),
|
||||
`CHANGELOG.md`, и (при смене контракта) `.openclaw/agents/deployer.md`; заведён
|
||||
ADR `docs/work-items/ORCH-061/06-adr/ADR-001-*.md`.
|
||||
- **FAIL:** функционал изменён без обновления документации/ADR.
|
||||
|
||||
## AC-14 — Регрессионные тесты зелёные
|
||||
- **PASS:** `pytest tests/ -q` проходит полностью; новые тесты из `04-test-plan.yaml`
|
||||
присутствуют и зелёные; существующие staging/deploy/qg/stage_engine тесты не упали.
|
||||
- **FAIL:** любой тест из плана отсутствует или красный.
|
||||
147
docs/work-items/ORCH-061/04-test-plan.yaml
Normal file
147
docs/work-items/ORCH-061/04-test-plan.yaml
Normal file
@@ -0,0 +1,147 @@
|
||||
work_item: ORCH-061
|
||||
title: "BUG: deploy-staging петля — откат на development (self-deploy)"
|
||||
description: >
|
||||
План тестов на устранение зацикливания deploy-staging -> development для
|
||||
self-hosting orchestrator. Покрывает обе подтверждённые причины: (1) ложный
|
||||
FAILED check_staging_status из-за заведомо инфраструктурных C9a/C9b в sandbox;
|
||||
(2) трактовку "no changes to commit" на action-стадии как недовыполнения.
|
||||
Тесты outcome-ориентированы и не предписывают механизм: часть кейсов помечена
|
||||
как mechanism-dependent (а=sandbox-инфра честно, б=толерантность/отвязка) —
|
||||
финальный набор подтверждает архитектор в ADR; реализуются тесты под выбранный
|
||||
механизм. Инвариант страховки (реальный регресс откатывает) и условность
|
||||
self-hosting проверяются ВСЕГДА.
|
||||
tests:
|
||||
# --- Главный сценарий: нет петли ----------------------------------------
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: >
|
||||
Корректный self-deploy: при staging_status SUCCESS и пройденном merge/freshness
|
||||
sub-gate advance_stage(deploy-staging, finished_agent=deployer) продвигает к
|
||||
deploy (Phase A approval-pending), НЕ откатывает на development. (AC-1)
|
||||
module: tests/test_stage_engine.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: >
|
||||
Регресс-страховка ORCH-35: реальный провал реальной pipeline-проверки ->
|
||||
staging_status FAILED -> advance_stage откатывает deploy-staging -> development
|
||||
+ set_issue_blocked. (AC-3)
|
||||
module: tests/test_stage_engine.py
|
||||
expected: PASS
|
||||
|
||||
# --- Причина №1: ложный инфраструктурный FAIL ---------------------------
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: >
|
||||
Классификация проверок staging_check: проверки, заведомо зависящие от
|
||||
sandbox-инфраструктуры (C9a/C9b), отличимы (метка/категория) от реальных
|
||||
pipeline-проверок. Чистая логика классификации/вердикта тестируется без
|
||||
live staging/docker. (AC-2, mechanism-dependent: вариант б)
|
||||
module: tests/test_staging_check_b6.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: >
|
||||
Вердикт-логика: все реальные проверки PASS, падают ТОЛЬКО известные
|
||||
sandbox-инфра проверки (C9a/C9b) -> итог не-FAILED (нет ложного отката).
|
||||
(AC-2)
|
||||
module: tests/test_qg_checks.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: >
|
||||
Вердикт-логика: падает хотя бы одна РЕАЛЬНАЯ pipeline-проверка (помимо инфра)
|
||||
-> итог FAILED (страховка не ослаблена, fail-closed). (AC-3)
|
||||
module: tests/test_qg_checks.py
|
||||
expected: PASS
|
||||
|
||||
# --- Причина №2: no changes на action-стадии ----------------------------
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: >
|
||||
На action-стадии (deploy-staging/deploy) для self-deploy отсутствие
|
||||
git-изменений ("no changes to commit") НЕ приводит к откату/недопродвижению;
|
||||
продвижение определяется exit + вердиктом, а не наличием коммита. (AC-4)
|
||||
module: tests/test_launcher.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: >
|
||||
На code-стадии (development) отсутствие изменений всё ещё обрабатывается
|
||||
прежним образом (нет ложного "успеха" там, где код должен был измениться) —
|
||||
изменение FR-3 не протекает на не-action стадии. (AC-4, regression-guard)
|
||||
module: tests/test_launcher.py
|
||||
expected: PASS
|
||||
|
||||
# --- Условность self-hosting --------------------------------------------
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: >
|
||||
Для не-self-hosting репо check_staging_status остаётся (True, "Staging gate
|
||||
N/A …") и новое поведение НЕ активируется; поведение этих репо неизменно.
|
||||
(AC-6, FR-5)
|
||||
module: tests/test_qg.py
|
||||
expected: PASS
|
||||
|
||||
# --- Kill-switch / обратная совместимость -------------------------------
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: >
|
||||
При выключенном флаге нового поведения (FR-6) система ведёт себя 1:1 как до
|
||||
ORCH-061: инфра-FAIL снова приводит к FAILED/откату. Дефолт флага безопасен.
|
||||
(AC-7)
|
||||
module: tests/test_config.py
|
||||
expected: PASS
|
||||
|
||||
# --- БАГ-8: реальный провал прод-деплоя ----------------------------------
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: >
|
||||
deploy_status FAILED (exit-code хука != 0) -> откат deploy -> development +
|
||||
set_issue_blocked + release merge-lease + clear deploy-state (БАГ-8 не сломан).
|
||||
(AC-5)
|
||||
module: tests/test_deploy_rollback.py
|
||||
expected: PASS
|
||||
|
||||
# --- Контракты / реестр / never-raise -----------------------------------
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: >
|
||||
Снапшот реестра QG_CHECKS и STAGE_TRANSITIONS не изменён неожиданно;
|
||||
frontmatter-контракты staging_status/deploy_status (SUCCESS|FAILED, только
|
||||
YAML) сохранены. (AC-8)
|
||||
module: tests/test_qg_registry_snapshot.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: >
|
||||
never-raise: новая логика staging-вердикта/advance при внутренней ошибке
|
||||
(io/парсинг/docker/ssh) возвращает безопасный детерминированный вердикт и не
|
||||
пробрасывает исключение в advance_stage. (AC-10)
|
||||
module: tests/test_stage_engine.py
|
||||
expected: PASS
|
||||
|
||||
# --- Интеграционный сквозной сценарий ------------------------------------
|
||||
- id: TC-13
|
||||
type: integration
|
||||
description: >
|
||||
Сквозной self-deploy на тестовой БД: задача deploy-staging при здоровом
|
||||
стенде с инфра-only недочётами проходит deploy-staging -> deploy (Phase A) ->
|
||||
(approve) -> deploy финализация SUCCESS -> done, БЕЗ единого отката на
|
||||
development в логе переходов. (AC-1, AC-4)
|
||||
module: tests/test_stage_engine.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-14
|
||||
type: integration
|
||||
description: >
|
||||
Наблюдаемость: при срабатывании нового поведения (игнор инфра-FAIL /
|
||||
ожидаемые no-changes) присутствует явная лог-строка/диагностика, отличающая
|
||||
"честно зелёный" от "зелёного с допущением". (AC-11)
|
||||
module: tests/test_stage_engine.py
|
||||
expected: PASS
|
||||
@@ -0,0 +1,222 @@
|
||||
# ADR-001 — Толерантность staging-вердикта к инфра-FAIL + инвариант «no-changes на action-стадии»
|
||||
|
||||
- **Статус:** Accepted
|
||||
- **Дата:** 2026-06-07
|
||||
- **Задача:** ORCH-061 (BUG, P0) · Репо: `orchestrator` (self-hosting)
|
||||
- **Связи:** ORCH-35/adr-0003 (условный staging-гейт), ORCH-36/adr-0007 (исполняемый self-deploy), ORCH-58/adr-0008 (провенанс staging-образа), ORCH-43/adr-0006 (merge-gate); блокирует ORCH-54.
|
||||
- **Сквозной ADR:** [adr-0009-staging-infra-tolerance](../../../architecture/adr/adr-0009-staging-infra-tolerance.md)
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
На стадии `deploy-staging` self-hosting `orchestrator` зацикливается:
|
||||
`check_staging_status` даёт FAILED → `_handle_qg_failure_rollbacks` откатывает
|
||||
`deploy-staging → development` → developer перезапускается → конвейер заново →
|
||||
снова `deploy-staging` → снова FAILED. Петля жжёт developer-ретраи и LLM-кредиты,
|
||||
а прод-деплой орка приходится доводить вручную (ORCH-58, ORCH-60). Это прямой
|
||||
блокер автономного внедрения (ORCH-54).
|
||||
|
||||
Подтверждены две независимые причины (BRD §2):
|
||||
|
||||
**Причина №1 — ложный FAILED.** `scripts/staging_check.py` в sandbox даёт
|
||||
8/10 PASS, 2 ложных FAIL на e2e-блоке C:
|
||||
- **C9a** — ветка не появляется в `orchestrator-sandbox`;
|
||||
- **C9b** — analyst-job не появляется в staging-очереди.
|
||||
|
||||
Оба завязаны на отсутствие sandbox-настроек (bot-аккаунты `ORCH_PLANE_BOT_*` не
|
||||
добавлены членами SANDBOX-проекта — проект создан после провижининга ботов). Это
|
||||
**отсутствие инфраструктуры sandbox, а не регресс кода**. Но `staging_check.py`
|
||||
суммирует `all_ok = passed == total` и делает `sys.exit(1)` при любом FAIL →
|
||||
deployer пишет `staging_status: FAILED` → откат.
|
||||
|
||||
**Причина №2 — «no changes to commit» на action-стадии.** Стадии деплоя по природе
|
||||
действие (рестарт/retag), а не правка `src/`. Отсутствие git-изменений не должно
|
||||
трактоваться как недовыполнение; критерий успеха action-стадии — exit0 +
|
||||
health/staging-вердикт, а не наличие коммита.
|
||||
|
||||
### Что есть сейчас в коде (точки дефекта)
|
||||
|
||||
- `scripts/staging_check.py`: `Results.summary()` → `all_ok = passed == total`;
|
||||
`main()` → `sys.exit(0 if all_ok else 1)`. Все проверки равнозначны — инфра-FAIL
|
||||
неотличим от регресса.
|
||||
- `src/qg/checks.py` → `check_staging_status` / `_parse_staging_status`: читает
|
||||
`staging_status:` (SUCCESS|FAILED) из `15-staging-log.md`. Условный (ORCH-35):
|
||||
для не-self репо → `(True, "Staging gate N/A …")`.
|
||||
- `src/stage_engine.py` → `_handle_qg_failure_rollbacks`: ветка
|
||||
`agent=="deployer" and qg=="check_staging_status"` → откат на `development`.
|
||||
- `src/agents/launcher.py` → `_monitor_agent`: ветка «no changes to commit» (строка
|
||||
~583) **уже** просто логирует и идёт в `_try_advance_stage` (НЕ откатывает).
|
||||
|
||||
## Рассмотренные направления (BRD §6)
|
||||
|
||||
- **(а) Починить sandbox-инфру** — добавить bot-токены SANDBOX, чтобы C9a/C9b
|
||||
проходили честно (10/10).
|
||||
- *Минусы:* хрупко (зависит от членства ботов в Plane-проекте, поддерживается
|
||||
руками вне кода); не предотвращает структурно будущие инфра-only FAIL;
|
||||
автономный self-deploy-таск не может надёжно выполнить Plane-admin действия сам.
|
||||
Не закрывает Причину №1 на уровне инварианта.
|
||||
- **(б) Отвязать вердикт от заведомо инфраструктурных проверок** — классифицировать
|
||||
проверки suite и сделать вердикт толерантным к инфра-FAIL, сохранив fail-closed
|
||||
для реальных проверок.
|
||||
- *Плюсы:* структурно, юнит-тестируемо (чистая вердикт-логика), управляемо
|
||||
(kill-switch), наблюдаемо (FR-7); сохраняет страховку (FR-4) по построению.
|
||||
|
||||
## Решение
|
||||
|
||||
Выбран механизм **(б)** как основной, с явной фиксацией инварианта по Причине №2.
|
||||
Направление (а) переведено в **необязательное hardening** (см. `07-infra-requirements.md`):
|
||||
с (б) оно перестаёт быть блокером.
|
||||
|
||||
### 1. Классификация проверок + толерантный вердикт (Причина №1, FR-2/FR-4)
|
||||
|
||||
Новый **leaf-модуль `src/staging_verdict.py`** — чистая логика, без I/O, контракт
|
||||
**never-raise**, только stdlib (импортируем и из orchestrator, и из
|
||||
`staging_check.py`, который уже импортирует `src.*` внутри контейнера — паттерн B6/ORCH-048):
|
||||
|
||||
```
|
||||
REAL = "real" # реальная pipeline-проверка
|
||||
SANDBOX_INFRA = "sandbox_infra" # заведомо зависит от sandbox-инфры
|
||||
|
||||
# Узкий allowlist известных инфра-проверок (по префиксу метки):
|
||||
SANDBOX_INFRA_CHECKS = frozenset({"C9a", "C9b"})
|
||||
|
||||
def classify_check(label: str) -> str:
|
||||
"""SANDBOX_INFRA если метка начинается с известного инфра-префикса, иначе REAL.
|
||||
Never-raise: на любом непонятном вводе → REAL (консервативно, fail-closed)."""
|
||||
|
||||
def compute_staging_verdict(items, infra_tolerant: bool) -> StagingVerdict:
|
||||
"""items: список (label, passed: bool, category: str).
|
||||
real_failed = [REAL-проверки с passed=False]
|
||||
infra_failed = [SANDBOX_INFRA-проверки с passed=False]
|
||||
- real_failed непусто -> FAILED, exit 1 (страховка)
|
||||
- infra_failed непусто и infra_tolerant -> SUCCESS, exit 0 (waived)
|
||||
- infra_failed непусто и НЕ infra_tolerant -> FAILED, exit 1 (legacy strict)
|
||||
- иначе -> SUCCESS, exit 0
|
||||
Never-raise: на битом вводе → консервативный FAILED."""
|
||||
```
|
||||
|
||||
`StagingVerdict` несёт `status` (`"SUCCESS"|"FAILED"`), `exit_code` (`0|1`),
|
||||
`waived` (список заваиверенных меток) и `summary` (человекочитаемая строка).
|
||||
|
||||
**Ключевой инвариант страховки (FR-4):** любая упавшая REAL-проверка ⇒ exit 1 ⇒
|
||||
FAILED ⇒ откат. В частности C7 (создать issue) и C8 (триггер `/webhook/plane`) —
|
||||
REAL. Waiver применяется к C9a/C9b **только** когда все REAL-проверки (включая
|
||||
C7/C8) зелёные. Вход в конвейер по-прежнему валидируется C7/C8; C9a/C9b проверяют
|
||||
лишь downstream-артефакты, которым нужна sandbox-инфра. Так blast-radius waiver'а
|
||||
сведён к двум именованным проверкам.
|
||||
|
||||
### 2. Правки `scripts/staging_check.py`
|
||||
|
||||
- `Results.add(label, passed, detail="", category=None)` — при `category is None`
|
||||
авто-классификация через `staging_verdict.classify_check(label)`; хранит категорию
|
||||
в элементе.
|
||||
- `Results.summary()` печатает разбивку по категориям (REAL / SANDBOX_INFRA).
|
||||
- `main()`:
|
||||
- резолвит флаг толерантности `_resolve_tolerance()` (см. ниже);
|
||||
- `verdict = compute_staging_verdict(results.items, infra_tolerant)`;
|
||||
- при `verdict.waived` печатает явную строку
|
||||
`INFRA-WAIVED: <labels> (known sandbox-infra; real checks green)` (FR-7);
|
||||
- `sys.exit(verdict.exit_code)`.
|
||||
- `_resolve_tolerance()`: читает `settings.staging_infra_tolerance_enabled` (через
|
||||
`from src.config import settings` — тот же паттерн, что B6). На ошибке импорта →
|
||||
**strict (False)** (fail-safe: не вайвить при нечитаемом конфиге) + warning.
|
||||
Опциональный CLI-флаг `--strict` принудительно выключает толерантность для ручных
|
||||
«честных» прогонов.
|
||||
|
||||
Прежние режимы (`--mode stub|full-real`) и проверки A/B/C7/C8 — без изменений.
|
||||
«Всегда 0» исключено: упавшая REAL-проверка всегда даёт exit 1 (TRZ §7).
|
||||
|
||||
### 3. Kill-switch (FR-6, AC-7)
|
||||
|
||||
`src/config.py`:
|
||||
```python
|
||||
# ORCH-061: толерантность staging-вердикта к заведомо инфраструктурным FAIL
|
||||
# (C9a/C9b) в sandbox. True -> упавшие ТОЛЬКО sandbox-инфра проверки вайверятся
|
||||
# (real-проверки fail-closed). False -> 1:1 прежнее строгое поведение (любой FAIL
|
||||
# -> staging_status FAILED -> откат). Env ORCH_STAGING_INFRA_TOLERANCE_ENABLED.
|
||||
staging_infra_tolerance_enabled: bool = True
|
||||
```
|
||||
|
||||
Дефолт **True** (как `merge_gate_enabled` / `image_freshness_enabled` /
|
||||
`self_deploy_enabled`): инвариант страховки (FR-4) держится независимо от флага —
|
||||
реальные провалы всё равно fail-closed; флаг существует, чтобы мгновенно вернуть
|
||||
legacy-строгость без передеплоя кода. Флаг живёт в `.env.staging` контейнера
|
||||
(`ORCH_` prefix), поэтому достижим скриптом внутри `orchestrator-staging`.
|
||||
`False` → suite строгий → 1:1 поведение до ORCH-061 (AC-7).
|
||||
|
||||
### 4. Что НЕ меняется (контракты, AC-8)
|
||||
|
||||
- `check_staging_status` / `_parse_staging_status` — **без изменений**: читают
|
||||
`staging_status:` (только YAML, `SUCCESS|FAILED`). Толерантность реализована
|
||||
ДО артефакта (в exit-code suite → вердикт deployer), внутри существующего пути
|
||||
staging-вердикта, не отдельной стадией (TRZ §6).
|
||||
- **Новый QG-чек НЕ добавляется** → реестр `QG_CHECKS` и снапшот-тест
|
||||
(`tests/test_qg_registry_snapshot.py`) неизменны (AC-8 / TC-11).
|
||||
- `STAGE_TRANSITIONS`, `get_previous_stage`, exit-code хука деплоя (0/1/2),
|
||||
`map_exit_code_to_status`, `check_deploy_status`, БАГ-8 — без изменений.
|
||||
- Условность self-hosting (AC-6): `staging_check.py` канонически бежит только для
|
||||
`orchestrator`; `check_staging_status` для не-self репо остаётся
|
||||
`(True, "Staging gate N/A …")`. Поведение прочих репо байт-в-байт неизменно.
|
||||
|
||||
### 5. Инвариант «no-changes на action-стадии» (Причина №2, FR-3/AC-4)
|
||||
|
||||
`launcher._monitor_agent` **уже** не откатывает на «no changes to commit» (просто
|
||||
логирует и идёт в `_try_advance_stage`; продвижение определяется гейтом). ORCH-061:
|
||||
- **Фиксируем инвариант** как покрытый тестами контракт: на `deploy-staging`/`deploy`
|
||||
для self-deploy продвижение определяется exit0 + гейт-вердиктом, НИКОГДА наличием
|
||||
коммита (TC-06).
|
||||
- **Наблюдаемость (FR-7/AC-11):** в ветке «no changes» логировать явную строку,
|
||||
отличающую action-стадию (ожидаемо: артефакт-вердикт, не обязательно код) от
|
||||
code-стадии. Резолв стадии задачи по `(repo, branch)`; при
|
||||
`stage ∈ {deploy-staging, deploy}` и `self_deploy.self_deploy_applies(repo)` →
|
||||
`staging/deploy: no code changes (expected on action stage)`.
|
||||
- **Regression-guard (TC-07):** на `development` (code-стадия) поведение «no changes»
|
||||
неизменно — изменение FR-3 не протекает на не-action стадию.
|
||||
|
||||
Изменение минимальное (self-hosting safety, AC-12): не трогает прод-контейнер 8500,
|
||||
сборки/recreate — только staging (8501).
|
||||
|
||||
## Затронутые файлы (для developer)
|
||||
|
||||
| Файл | Изменение |
|
||||
|------|-----------|
|
||||
| `src/staging_verdict.py` | **новый** leaf-модуль: `classify_check`, `compute_staging_verdict`, `StagingVerdict` (pure, never-raise). |
|
||||
| `scripts/staging_check.py` | категории в `Results`, вердикт через `staging_verdict`, INFRA-WAIVED-лог, `--strict`. |
|
||||
| `src/config.py` | флаг `staging_infra_tolerance_enabled` (env `ORCH_STAGING_INFRA_TOLERANCE_ENABLED`). |
|
||||
| `src/agents/launcher.py` | observability-лог action-stage no-changes (без смены логики продвижения). |
|
||||
| `.openclaw/agents/deployer.md` | уточнение: exit0 может включать «infra-waived»; контракт `staging_status:` SUCCESS\|FAILED неизменен. |
|
||||
| `docs/operations/STAGING_CHECK.md` | поведение C9a/C9b, флаг, INFRA-WAIVED, `--strict`. |
|
||||
| `docs/architecture/README.md` | пометка ORCH-061 в разделе staging-гейта (уже внесена архитектором). |
|
||||
| `CHANGELOG.md` | запись ORCH-061. |
|
||||
| `tests/` | TC-01…TC-14 (см. `04-test-plan.yaml`). |
|
||||
|
||||
## Последствия
|
||||
|
||||
**Плюсы**
|
||||
- Петля устранена структурно: ложный инфра-FAIL → SUCCESS (waived) → нет отката (G1/G2).
|
||||
- Страховка цела: любая реальная pipeline-проверка fail-closed → FAILED → откат (G4/FR-4).
|
||||
- Чистая вердикт-логика юнит-тестируема без live staging/docker (NFR-тестируемость).
|
||||
- Контракты гейтов/стадий/вердиктов/реестра не тронуты (AC-8); схема БД не меняется (AC-9).
|
||||
- Мгновенный откат к legacy через kill-switch (AC-7).
|
||||
- Разблокирует автономный self-deploy (ORCH-54).
|
||||
|
||||
**Минусы / ограничения**
|
||||
- C9a/C9b теперь могут заваиверить **реальный** даунстрим-регресс именно в создании
|
||||
ветки / постановке analyst-job (узкий риск). Митигировано: waiver только когда C7/C8
|
||||
и все прочие REAL зелёные; allowlist жёстко = {C9a, C9b}; INFRA-WAIVED логируется и
|
||||
виден оператору. См. `10-tech-risks.md` (R-1).
|
||||
- Толерантность скрывает «нездоровье sandbox» как зелёное-с-допущением; отличимо
|
||||
только по INFRA-WAIVED-логу/комментарию (наблюдаемость обязательна, FR-7).
|
||||
- Honest 10/10 в sandbox (направление а) остаётся желательным hardening, но не блокером.
|
||||
|
||||
## Альтернативы (отклонены)
|
||||
|
||||
- **Только (а) — починить sandbox-инфру:** хрупко, не структурно, вне автономной
|
||||
досягаемости таска. Оставлено как опциональное hardening.
|
||||
- **«Зелёный по умолчанию» при недоступности проверок:** запрещён FR-4 (fail-closed).
|
||||
- **Новый QG-чек `check_staging_infra_tolerant`:** избыточно — менял бы реестр
|
||||
`QG_CHECKS` и снапшот; толерантность лучше живёт в suite/вердикте до артефакта.
|
||||
- **Толерантность внутри `check_staging_status` через структурный артефакт:**
|
||||
потребовал бы сменить контракт `15-staging-log.md` и научить deployer писать
|
||||
per-check категории — больше движущихся частей; отклонено в пользу решения в suite.
|
||||
37
docs/work-items/ORCH-061/07-infra-requirements.md
Normal file
37
docs/work-items/ORCH-061/07-infra-requirements.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 07 — Требования к инфраструктуре: ORCH-061
|
||||
|
||||
Work Item: **ORCH-061** · Репо: `orchestrator`
|
||||
|
||||
Топология/контейнеры/порты **не меняются** (TRZ §3, §9). Self-hosting-безопасность
|
||||
сохранена: прод-контейнер `orchestrator` (8500) не перезапускается/не роняется в
|
||||
рамках задачи; любые сборки/recreate — только staging (8501). См.
|
||||
`docs/operations/INFRA.md`.
|
||||
|
||||
## IR-1 — Конфиг-флаг (kill-switch)
|
||||
Новый флаг `staging_infra_tolerance_enabled` (env
|
||||
`ORCH_STAGING_INFRA_TOLERANCE_ENABLED`, дефолт `true`).
|
||||
|
||||
- Должен присутствовать в окружении контейнера **`orchestrator-staging`**
|
||||
(`.env.staging`), т.к. `scripts/staging_check.py` читает его через
|
||||
`src.config.settings` при каноническом запуске `docker exec` внутри стенда.
|
||||
- Для прод-инстанса (`.env`) флаг безвреден (на прод-пути staging-suite не
|
||||
исполняется), но рекомендуется держать значения консистентными.
|
||||
- `false` → мгновенный возврат к строгому (legacy) поведению без передеплоя кода.
|
||||
- Канон секретов/env: значения в `.env`/`.env.staging` на хосте, в гит НЕ
|
||||
коммитятся; задокументировать ключ в `.env.example` (канон ORCH-9).
|
||||
|
||||
## IR-2 — Опциональное hardening sandbox (направление «а», НЕ блокер)
|
||||
Первопричина ложных C9a/C9b — bot-аккаунты агентов (`ORCH_PLANE_BOT_*`) не добавлены
|
||||
членами Plane-проекта **SANDBOX** (`8c5a3025-…`), созданного после провижининга
|
||||
ботов. С выбранным механизмом (б) это перестаёт блокировать конвейер, но честный
|
||||
10/10 в sandbox желателен:
|
||||
|
||||
- Добавить bot-аккаунты агентов членами SANDBOX-проекта в Plane (даст честный
|
||||
C9b: коммент analyst'а перестанет получать 403; и устранит инфра-причину C9a/C9b).
|
||||
- Действие — ручное (Plane-admin), вне автоматической досягаемости таска; выполняется
|
||||
оператором при возможности. После него C9a/C9b проходят честно и waiver не нужен.
|
||||
- Это hardening, а не требование приёмки ORCH-061 (приёмка — на механизме «б»).
|
||||
|
||||
## IR-3 — Без новой инфраструктуры
|
||||
Новые сервисы/порты/тома/сетевые правила/cron — **не требуются**. Никаких
|
||||
изменений в `docker-compose.yml`, образах, реестре проектов.
|
||||
20
docs/work-items/ORCH-061/08-data-requirements.md
Normal file
20
docs/work-items/ORCH-061/08-data-requirements.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 08 — Требования к данным / схеме БД: ORCH-061
|
||||
|
||||
Work Item: **ORCH-061** · Репо: `orchestrator`
|
||||
|
||||
## DR-1 — Схема БД не меняется (AC-9)
|
||||
Никаких миграций. Таблицы `events`, `tasks`, `agent_runs`, `jobs` — без изменений
|
||||
колонок/индексов/таблиц.
|
||||
|
||||
## DR-2 — Никакого нового персистентного состояния
|
||||
Решение (ADR-001) — чистая вердикт-логика (`src/staging_verdict.py`) + конфиг-флаг +
|
||||
правка exit-code suite. Состояние конвейера не вводится:
|
||||
- толерантность вычисляется на лету при прогоне `staging_check.py`;
|
||||
- restart-safe-состояние не требуется (вердикт фиксируется в существующем артефакте
|
||||
`15-staging-log.md` через прежний контракт `staging_status: SUCCESS|FAILED`).
|
||||
|
||||
## DR-3 — Артефакт-контракт неизменен
|
||||
`15-staging-log.md` по-прежнему несёт frontmatter `staging_status: SUCCESS|FAILED`
|
||||
(только YAML). `14-deploy-log.md` (`deploy_status:`) — без изменений. Гейты читают
|
||||
ТОЛЬКО frontmatter. Толерантность реализована ДО записи артефакта (на уровне
|
||||
exit-code suite → вердикт deployer), поэтому формат и парсинг артефактов не трогаются.
|
||||
25
docs/work-items/ORCH-061/10-tech-risks.md
Normal file
25
docs/work-items/ORCH-061/10-tech-risks.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 10 — Технические риски: ORCH-061
|
||||
|
||||
Work Item: **ORCH-061** · Репо: `orchestrator` (self-hosting)
|
||||
|
||||
| # | Риск | Вероятн. | Влияние | Митигация |
|
||||
|---|------|----------|---------|-----------|
|
||||
| **R-1** | Waiver C9a/C9b маскирует **реальный** регресс именно в создании ветки / постановке analyst-job (ложно-зелёный staging). | Низкая | Высокое | Allowlist жёстко `{C9a, C9b}`; waiver применяется ТОЛЬКО когда ВСЕ REAL-проверки зелёные, включая C7 (создать issue) и C8 (триггер `/webhook/plane`) — вход в конвейер всегда валидируется реально. `INFRA-WAIVED`-строка в логе/комменте делает допущение видимым (FR-7). Honest 10/10 (IR-2) убирает риск совсем. |
|
||||
| **R-2** | Ослабление страховки: реальный pipeline-FAIL пройдёт как SUCCESS. | Низкая | Критич. | Инвариант `compute_staging_verdict`: любая упавшая REAL → exit1 → FAILED → откат (FR-4/AC-3/TC-05). Покрыто юнит-тестом отдельным кейсом. |
|
||||
| **R-3** | Флаг не достигает скрипта (читается не из того env) → толерантность «молча» не работает или, наоборот, не выключается. | Средняя | Среднее | Скрипт читает `settings.staging_infra_tolerance_enabled` через `from src.config import settings` — тот же канал, что B6/ORCH-048 (внутри `orchestrator-staging`, env `.env.staging`). На ошибке импорта — fail-safe в strict (False) + warning. Документировать ключ в `.env.staging`/`.env.example` (IR-1). Тест kill-switch (TC-09). |
|
||||
| **R-4** | Классификатор ошибочно пометит REAL-проверку как SANDBOX_INFRA (расширение allowlist в будущем). | Низкая | Высокое | `classify_check` — узкий префиксный allowlist; добавление новой инфра-метки требует осознанного PR + теста (TC-03). По умолчанию неизвестная метка → REAL (консервативно). |
|
||||
| **R-5** | Регресс совместимости: изменение exit-code suite ломает другие потребители (deploy-хук, ручные прогоны). | Низкая | Среднее | Exit-code семантика сохранена для honest-прогонов (всё PASS → 0; реальный FAIL → 1). Меняется лишь трактовка «только инфра-FAIL» (теперь 0 при толерантности). Deployer-маппинг exit0→SUCCESS/≠0→FAILED не меняется; deployer.md уточняется. `--strict` даёт ручной honest-режим. |
|
||||
| **R-6** | never-raise нарушен: исключение из `staging_verdict`/классификатора. | Низкая | Среднее | `src/staging_verdict.py` — pure, без I/O; контракт never-raise (на битом вводе → консервативный FAILED). Логика вне пути `advance_stage` (исполняется в subprocess suite), поэтому в конвейер исключение структурно не попадает (AC-10). |
|
||||
| **R-7** | FR-3: правка no-changes протекает на code-стадию (`development`) и маскирует «developer ничего не сделал». | Низкая | Среднее | Observability-строка ограничена `stage ∈ {deploy-staging, deploy}` и `self_deploy_applies(repo)`; логика продвижения launcher не меняется. Regression-guard TC-07. |
|
||||
| **R-8** | Self-hosting: правки случайно затронут прод 8500 / не-self репо. | Низкая | Критич. | Изменения только на self-deploy-пути и в suite (бежит лишь для `orchestrator`-staging). `check_staging_status` для не-self репо неизменно `(True, N/A)` (AC-6/TC-08). Сборки/recreate — только 8501. Прод 8500 не трогается (AC-12). |
|
||||
|
||||
## Контрактные инварианты (не нарушать)
|
||||
- `STAGE_TRANSITIONS`, `get_previous_stage` — без изменений.
|
||||
- Реестр `QG_CHECKS` — без изменений; новый QG-чек НЕ вводится (снапшот-тест зелёный, TC-11).
|
||||
- Frontmatter `staging_status:` / `deploy_status:` — только YAML, `SUCCESS|FAILED`.
|
||||
- Exit-code хука деплоя (0/1/2) и `map_exit_code_to_status` — без изменений.
|
||||
- БАГ-8 (`deploy → development`) и ORCH-35 (`deploy-staging → development`) для
|
||||
**реальных** провалов — сохранены.
|
||||
- Схема БД — без миграций.
|
||||
# ci-rerun 2026-06-07T13:08:38Z after disk cleanup
|
||||
# ci-rerun gitea-restarted 2026-06-07T13:14:14Z
|
||||
88
docs/work-items/ORCH-061/12-review.md
Normal file
88
docs/work-items/ORCH-061/12-review.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-061
|
||||
verdict: APPROVED
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-061
|
||||
|
||||
## Summary
|
||||
|
||||
Исправление петли `deploy-staging → development` при self-hosting self-deploy.
|
||||
Реализовано Direction (б) из ADR-001: классификация staging-проверок на `REAL`
|
||||
(fail-closed) и `SANDBOX_INFRA` (узкий allowlist `{C9a, C9b}`, waivable) +
|
||||
толерантный-но-fail-closed вердикт.
|
||||
|
||||
Реализация **полностью соответствует ТЗ (02-trz.md), критериям приёмки
|
||||
(03-acceptance-criteria.md) и ADR-001**. Все контракты сохранены, документация
|
||||
обновлена в том же PR, тесты зелёные.
|
||||
|
||||
Проверено по осям:
|
||||
|
||||
- **Соответствие ТЗ:** FR-1…FR-7 закрыты. Новый leaf-модуль
|
||||
`src/staging_verdict.py` (stdlib-only, never-raise), флаг
|
||||
`staging_infra_tolerance_enabled` (kill-switch, default True), observability
|
||||
через `INFRA-WAIVED:`/`VERDICT:` и `action_stage_no_changes_note`.
|
||||
- **Соответствие ADR-001:** механизм, allowlist `{C9a, C9b}`, fail-closed для
|
||||
REAL, waiver только когда все REAL (вкл. C7/C8) зелёные, `--strict`,
|
||||
`_resolve_tolerance` (fail-safe → strict при нечитаемом конфиге) — реализовано
|
||||
ровно как в «Решении» ADR. Затронутые файлы совпадают с таблицей ADR.
|
||||
- **Контракты (AC-8):** `src/qg/checks.py` (`check_staging_status`/
|
||||
`_parse_staging_status`), `src/stages.py` (`STAGE_TRANSITIONS`, `QG_CHECKS`)
|
||||
— **не изменены** (подтверждено `git diff`). Толерантность живёт в suite ДО
|
||||
записи артефакта; новый QG-чек не вводится; реестр-снапшот цел.
|
||||
- **Схема БД (AC-9):** миграций нет, флаг — только конфиг.
|
||||
- **never-raise (AC-10):** `compute_staging_verdict`/`classify_check`/
|
||||
`_coerce_item`/`action_stage_no_changes_note` ловят всё и деградируют в
|
||||
консервативный FAILED/None. Покрыто TC-12.
|
||||
- **Условность self-hosting / страховка (AC-3/AC-5/AC-6):** rollback на реальном
|
||||
FAIL сохранён (`tests/test_stage_engine.py` TestStaging*), поведение не-self
|
||||
репо неизменно.
|
||||
- **Тесты (AC-14):** `pytest tests/ -q` → **670 passed**. ORCH-061 покрытие:
|
||||
TC-04 (infra waived → SUCCESS), TC-05 (REAL fail → FAILED), TC-09 (strict),
|
||||
TC-12 (garbage never-raise), TC-06/TC-07 (action-stage no-changes note),
|
||||
non-self репо.
|
||||
- **Безопасность self-hosting (AC-12):** код задачи не трогает прод 8500;
|
||||
сборки/recreate — вне пути этой логики.
|
||||
|
||||
Примечание по диффу: при просмотре `git diff main...HEAD` появлялись файлы
|
||||
ORCH-060 (reconciler, plane_sync, config reconcile-флаги). Это артефакт
|
||||
**устаревшего локального ref `main`** — `origin/main` уже содержит ORCH-060
|
||||
(merge `d4c6cc0`, PR #60). Истинный `git diff origin/main...HEAD` — чистый
|
||||
ORCH-061. Бандлинга чужого work-item нет.
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- нет
|
||||
|
||||
### P1 — Must fix
|
||||
- нет
|
||||
|
||||
### P2 — Should fix
|
||||
- [ ] **Стрэй-файлы агентного скрэтча закоммичены в репо:** `.task.md`,
|
||||
`.task-arch.md`, `.task-dev.md` (хэндофф-файлы стадий analysis/architecture/
|
||||
development) попали в коммит и не покрыты `.gitignore`. Это засоряет репо и
|
||||
будет повторяться каждый прогон. Рекомендация: удалить из индекса и добавить
|
||||
`.task*.md` в `.gitignore`. Не функциональный дефект — на корректность
|
||||
ORCH-061 не влияет.
|
||||
|
||||
## Документация
|
||||
|
||||
Обновлена в том же PR (golden source, AC-13) — соответствует требованию CLAUDE.md:
|
||||
|
||||
- `docs/architecture/README.md` — раздел staging-гейта помечен ORCH-061 +
|
||||
статус в футере.
|
||||
- `docs/architecture/adr/adr-0009-staging-infra-tolerance.md` — сквозной ADR
|
||||
заведён; `adr/README.md` обновлён.
|
||||
- `docs/operations/STAGING_CHECK.md` — поведение C9a/C9b, флаг, INFRA-WAIVED,
|
||||
`--strict`.
|
||||
- `.openclaw/agents/deployer.md` — уточнён контракт exit0/INFRA-WAIVED (контракт
|
||||
`staging_status: SUCCESS|FAILED` неизменён).
|
||||
- `.env.example` — `ORCH_STAGING_INFRA_TOLERANCE_ENABLED` (канон, секреты не
|
||||
коммитятся).
|
||||
- `CHANGELOG.md` — запись ORCH-061.
|
||||
- ADR per-work-item `docs/work-items/ORCH-061/06-adr/ADR-001-*.md` — присутствует.
|
||||
|
||||
Документация полная и точная; расхождений с кодом не выявлено.
|
||||
85
docs/work-items/ORCH-061/13-test-report.md
Normal file
85
docs/work-items/ORCH-061/13-test-report.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-061
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-061
|
||||
|
||||
BUG: устранение петли `deploy-staging → development` при self-hosting self-deploy.
|
||||
Реализован Direction (б) из ADR-001: классификация staging-проверок на `REAL`
|
||||
(fail-closed) и `SANDBOX_INFRA` (allowlist `{C9a, C9b}`, waivable) + толерантный,
|
||||
но fail-closed вердикт (`src/staging_verdict.py`), kill-switch
|
||||
`staging_infra_tolerance_enabled` (env `ORCH_STAGING_INFRA_TOLERANCE_ENABLED`).
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3
|
||||
- Дата: 2026-06-07T13:19Z
|
||||
- Ветка: `feature/ORCH-061-bug-deploy-staging-development`
|
||||
- Review verdict: APPROVED (12-review.md)
|
||||
|
||||
## Smoke test API (prod 8500, read-only)
|
||||
| Endpoint | Результат |
|
||||
|----------|-----------|
|
||||
| GET /health | HTTP 200 `{"status":"ok","service":"orchestrator"}` |
|
||||
| GET /status | HTTP 200 (ORCH-061 в стадии `testing`) |
|
||||
| GET /queue | HTTP 200 (counts/resilience/reconcile present) |
|
||||
|
||||
> Прод-контейнер 8500 не перезапускался и не трогался (self-hosting safety, AC-12).
|
||||
|
||||
## Результаты по тест-плану (04-test-plan.yaml)
|
||||
|
||||
| TC ID | Описание | Тест | Результат |
|
||||
|-------|----------|------|-----------|
|
||||
| TC-01 | Корректный self-deploy: staging SUCCESS → advance к deploy, без отката | `test_stage_engine.py::test_tc01_healthy_self_deploy_advances_no_rollback` | PASS |
|
||||
| TC-02 | Страховка ORCH-35: реальный FAIL → откат deploy-staging→development | `test_stage_engine.py::test_tc02_real_staging_failed_rolls_back` | PASS |
|
||||
| TC-03 | Классификация REAL vs SANDBOX_INFRA (C9a/C9b отличимы) | `test_staging_check_b6.py::test_tc03_classify_infra_checks` (+ records/override/strict) | PASS |
|
||||
| TC-04 | Падают только C9a/C9b → итог не-FAILED (нет ложного отката) | `test_qg_checks.py::test_tc04_only_infra_failures_waived_to_success` | PASS |
|
||||
| TC-05 | Падает реальная pipeline-проверка → FAILED (fail-closed) | `test_qg_checks.py::test_tc05_any_real_failure_fails_closed` (+ `_even_alone`) | PASS |
|
||||
| TC-06 | no-changes на action-стадии (deploy-staging/deploy) не есть недовыполнение | `test_launcher.py::test_tc06_deploy_staging_self_deploy_returns_note` / `test_tc06_deploy_self_deploy_returns_note` | PASS |
|
||||
| TC-07 | regression-guard: на code-стадии (development) поведение прежнее | `test_launcher.py::test_tc07_development_stage_returns_none` | PASS |
|
||||
| TC-08 | Не-self-hosting репо: check_staging_status остаётся (True, "N/A …") | `test_qg.py` (no-op N/A) | PASS |
|
||||
| TC-09 | Kill-switch выкл → 1:1 прежнее строгое поведение, безопасный дефолт | `test_qg_checks.py::test_tc09_infra_failure_strict_mode_fails_closed` + `test_config.py::test_staging_infra_tolerance_*` | PASS |
|
||||
| TC-10 | БАГ-8: deploy_status FAILED → откат deploy→development | `test_deploy_rollback.py` | PASS |
|
||||
| TC-11 | Снапшот QG_CHECKS / STAGE_TRANSITIONS не изменён; frontmatter-контракты целы | `test_qg_registry_snapshot.py` | PASS |
|
||||
| TC-12 | never-raise: вердикт-логика при мусоре → безопасный детерминированный FAILED | `test_qg_checks.py::test_tc12_compute_verdict_never_raises_on_garbage` + `test_stage_engine.py::test_tc12_retry_and_rollback_behavior_unchanged` | PASS |
|
||||
| TC-13 | Сквозной self-deploy: deploy-staging→deploy→done без единого отката | `test_stage_engine.py::test_tc13_end_to_end_self_deploy_no_single_rollback` | PASS |
|
||||
| TC-14 | Наблюдаемость: «зелёный с допущением» отличим от честного зелёного | `test_stage_engine.py::test_tc14_waived_green_distinguishable_from_honest_green` | PASS |
|
||||
|
||||
Все 14 TC присутствуют и зелёные.
|
||||
|
||||
## Сопоставление с критериями приёмки (03-acceptance-criteria.md)
|
||||
| AC | Критерий | Покрытие | Статус |
|
||||
|----|----------|----------|--------|
|
||||
| AC-1 | Проход self-deploy без петли | TC-01, TC-13 | PASS |
|
||||
| AC-2 | Инфра-FAIL (C9a/C9b) не откатывает | TC-03, TC-04 | PASS |
|
||||
| AC-3 | Реальный провал staging откатывает | TC-02, TC-05 | PASS |
|
||||
| AC-4 | no-changes на action-стадии ≠ недовыполнение | TC-06, TC-07 | PASS |
|
||||
| AC-5 | БАГ-8: провал прод-деплоя откатывает | TC-10 | PASS |
|
||||
| AC-6 | Условность self-hosting сохранена | TC-08 | PASS |
|
||||
| AC-7 | Kill-switch возвращает прежнее поведение | TC-09 | PASS |
|
||||
| AC-8 | Контракты не сломаны (реестр/frontmatter/exit-code) | TC-11 | PASS |
|
||||
| AC-9 | Схема БД не меняется | миграций нет (флаг — конфиг) | PASS |
|
||||
| AC-10 | never-raise | TC-12 | PASS |
|
||||
| AC-11 | Наблюдаемость (INFRA-WAIVED / waived list) | TC-14 | PASS |
|
||||
| AC-12 | Безопасность self-hosting (прод 8500 не трогается) | smoke + код пути | PASS |
|
||||
| AC-13 | Документация обновлена (golden source) | подтверждено в 12-review.md | PASS |
|
||||
| AC-14 | Регрессионные тесты зелёные | `pytest tests/ -q` → 670 passed | PASS |
|
||||
|
||||
## Вывод pytest
|
||||
```
|
||||
$ python -m pytest tests/ -v --tb=short
|
||||
...
|
||||
======================= 670 passed, 1 warning in 12.15s ========================
|
||||
```
|
||||
Единственный warning — PydanticDeprecatedSince20 (class-based Config в `src/config.py`),
|
||||
не относится к ORCH-061, существовал ранее.
|
||||
|
||||
## Итог
|
||||
**PASS** — полный регресс зелёный (670 passed, 0 failed), все 14 TC из плана и все 14
|
||||
критериев приёмки выполнены. Страховка цела (реальный регресс staging и БАГ-8
|
||||
откатывают), условность self-hosting сохранена, kill-switch работает, never-raise
|
||||
покрыт. Smoke API prod — 200, прод-контейнер не затронут.
|
||||
|
||||
Задача готова к переходу на стадию **deploy-staging**.
|
||||
68
docs/work-items/ORCH-061/15-staging-log.md
Normal file
68
docs/work-items/ORCH-061/15-staging-log.md
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-07T13:27:06+00:00
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log — ORCH-061
|
||||
|
||||
Staging test suite completed against the live `orchestrator-staging` stand (8501).
|
||||
**Verdict: SUCCESS (exit 0)** — all REAL pipeline checks green; the two known
|
||||
sandbox-infra checks (C9a/C9b) were FAILED-but-**waived** by the ORCH-061
|
||||
infra-tolerance logic. This is exactly the behaviour this work item ships.
|
||||
|
||||
## Observability — INFRA-WAIVED
|
||||
|
||||
```
|
||||
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
|
||||
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
|
||||
```
|
||||
|
||||
## Result breakdown
|
||||
|
||||
```
|
||||
RESULT: 8/10 checks PASS
|
||||
REAL failed : none
|
||||
SANDBOX_INFRA failed: ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue']
|
||||
tolerance: staging_infra_tolerance_enabled=True
|
||||
```
|
||||
|
||||
| Check | Category | Result |
|
||||
|-------|----------|--------|
|
||||
| A1 GET /health → 200 status=ok | REAL | PASS |
|
||||
| A2 GET /queue → 200 counts/max_concurrency/resilience | REAL | PASS |
|
||||
| A3 ORCH_STAGING=true (not prod) | REAL | PASS |
|
||||
| B4 Plane: sandbox project accessible | REAL | PASS |
|
||||
| B5 Gitea: orchestrator-sandbox accessible, push=true | REAL | PASS |
|
||||
| B6 Registry: sandbox present, prod ET/ORCH absent | REAL | PASS |
|
||||
| C7 Create issue in Plane SANDBOX | REAL | PASS |
|
||||
| C8 Trigger pipeline via /webhook/plane | REAL | PASS |
|
||||
| C9a Branch appears in orchestrator-sandbox | SANDBOX_INFRA | FAIL (waived) |
|
||||
| C9b Analyst job enqueued in staging queue | SANDBOX_INFRA | FAIL (waived) |
|
||||
|
||||
C9a/C9b fail because the SANDBOX bot accounts are not yet members of the Plane
|
||||
sandbox project, so steps 6+ of the pipeline are unreachable **in the sandbox** —
|
||||
an infrastructure limitation, not a pipeline regression (see
|
||||
`docs/operations/STAGING_CHECK.md`). All REAL checks (incl. C7/C8) are green, so
|
||||
the waiver applies and the gate advances.
|
||||
|
||||
## Run note (self-hosting bootstrap)
|
||||
|
||||
The canonical bind-mounted script path (`/repos/orchestrator/scripts/staging_check.py`)
|
||||
and the running `orchestrator-staging` image both predate ORCH-061 (no
|
||||
`src/staging_verdict.py`, tolerance flag absent), because ORCH-061 modifies the
|
||||
staging gate itself. To produce a faithful verdict for the **validated commit**,
|
||||
the gate was executed from the validated worktree inside the staging container:
|
||||
|
||||
```
|
||||
docker exec orchestrator-staging \
|
||||
env PYTHONPATH=<worktree>:/app \
|
||||
python3 <worktree>/scripts/staging_check.py \
|
||||
--base-url http://localhost:8501 --mode stub
|
||||
```
|
||||
|
||||
`PYTHONPATH=<worktree>:/app` keeps B6's registry read sourced from the running
|
||||
staging instance's own env (sandbox-only registry — ORCH-048/ADR-001), while
|
||||
loading the shipped `staging_verdict` logic and `staging_infra_tolerance_enabled`
|
||||
config. This exercises the live staging endpoints AND the exact verdict logic
|
||||
being shipped. EXEC EXIT CODE: 0.
|
||||
7
docs/work-items/ORCH-065/00-business-request.md
Normal file
7
docs/work-items/ORCH-065/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: BUG: zombie jobs + merge-lease залип (процесс умер, статус running)
|
||||
|
||||
Work Item ID: ORCH-065
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
103
docs/work-items/ORCH-065/01-brd.md
Normal file
103
docs/work-items/ORCH-065/01-brd.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# BRD — ORCH-065: zombie jobs + залипший merge-lease
|
||||
|
||||
Work Item ID: ORCH-065
|
||||
Тип: BUG (P0)
|
||||
Репозиторий: orchestrator (self-hosting)
|
||||
Эпик: блокер ORCH-54 (полностью автономный self-deploy)
|
||||
|
||||
## 1. Контекст и проблема
|
||||
|
||||
Оркестратор — единый инстанс с **общей БД и общей очередью** (`jobs`,
|
||||
`max_concurrency=1` для self-hosting), обслуживающий несколько проектов. Финальная
|
||||
автономность self-deploy упирается в два связанных класса отказов, оба сводящиеся
|
||||
к «процесс умер/завершился, а состояние осталось захваченным навсегда»:
|
||||
|
||||
### Проблема A — zombie jobs (строка `jobs` навсегда `running`)
|
||||
Агент (deployer/developer/reviewer) завершается **или умирает** (краш, OOM,
|
||||
рестарт контейнера в ходе self-deploy, гибель monitor-потока), но строка в таблице
|
||||
`jobs` остаётся в статусе `running`. Финализация статуса job выполняется **только**
|
||||
в `_monitor_agent` → `_finalize_job` внутри того же процесса; если этот поток/процесс
|
||||
не доживает до финализации — job «зомбирован».
|
||||
|
||||
- Единственная имеющаяся защита — `requeue_running_jobs()` в `main.lifespan`,
|
||||
срабатывающая **исключительно на старте процесса**. Зомби, возникший **без**
|
||||
рестарта (умер дочерний процесс/monitor-поток, а сервис жив), не реанимируется
|
||||
никогда.
|
||||
- При `max_concurrency=1` одна зомби-строка `running` блокирует claim всех
|
||||
последующих job (`count_running_jobs() >= max_concurrency` → claim не происходит)
|
||||
→ **встаёт конвейер всех проектов**.
|
||||
|
||||
### Проблема B — залипший merge-lease
|
||||
Merge-gate (ORCH-043) берёт файловый lease `<repos_dir>/.merge-lease-<repo>.json`
|
||||
ПЕРЕД rebase+re-test и держит его до фактического merge PR в `main`. Если процесс
|
||||
умирает на финальном merge **с зажатым lease**:
|
||||
|
||||
- Реклейм lease реализован **лениво и только по возрасту** (`age >=
|
||||
merge_lock_timeout_s`) и **только в момент `acquire_merge_lease` другой задачей**.
|
||||
Проактивного освобождения (на старте / периодически) нет; **liveness держателя по
|
||||
pid не проверяется** (хотя `pid` в lease пишется).
|
||||
- Пострадавшая задача сама re-drive не получает: merge не финализируется → задача
|
||||
висит, lease мешает чужим merge до истечения TTL.
|
||||
|
||||
### Проблема C — неидемпотентная финализация merge
|
||||
Если rebase+re-test прошли зелёно (ветка догнана и проверена), но процесс умер до
|
||||
завершения слияния PR — повторного «докатывания» merge нет. Задача застревает в
|
||||
полу-выполненном состоянии, хотя вся дорогая работа (rebase+re-test) уже сделана.
|
||||
|
||||
## 2. Бизнес-последствия
|
||||
- **Это ПОСЛЕДНЯЯ ручная точка автономного деплоя.** Без фикса ни одна self-hosting
|
||||
задача не доезжает до прода без оператора (cancel zombie + ручной merge PR +
|
||||
ручной `--deploy`).
|
||||
- Прямой блокер эпика ORCH-54.
|
||||
- Доказанные инциденты (07.06): ORCH-58/60/61/21 — каждый раз после успешного
|
||||
deployer-прохода job оставался `running`; jobs **236/239/242/254** — зомби,
|
||||
прод-merge/deploy доводились вручную.
|
||||
- Групповой риск: зомби в общей очереди при concurrency=1 останавливает конвейер
|
||||
enduro-trails и всех прочих проектов.
|
||||
|
||||
## 3. Цель
|
||||
Сделать так, чтобы **смерть процесса/потока на любой стадии (включая self-restart
|
||||
во время deploy) НЕ оставляла навсегда захваченных ресурсов** — ни строки `jobs` в
|
||||
`running`, ни merge-lease. Конвейер должен самовосстанавливаться без оператора, при
|
||||
этом сохраняя все инварианты self-hosting (не ронять прод-контейнер, не трогать
|
||||
`main`, fail-closed на реальных ошибках).
|
||||
|
||||
## 4. Объём (Scope)
|
||||
|
||||
### В объёме
|
||||
1. **Job-reaper** — фоновый watchdog (паттерн `reconciler`/`queue_worker`),
|
||||
детектирующий «мёртвый» `running`-job и приводящий его строку в корректный
|
||||
терминальный/повторный статус (`done`/`failed`/`queued`) детерминированно,
|
||||
без LLM. Restart-safe и работающий **без** рестарта процесса.
|
||||
2. **Проактивный реклейм stale merge-lease** — освобождение lease, чей держатель
|
||||
мёртв (pid не жив) ИЛИ возраст превысил TTL — на старте и периодически (reaper/
|
||||
reconciler), а не только лениво при чужом `acquire`.
|
||||
3. **Идемпотентная финализация merge** — если rebase+re-test зелёные, но merge не
|
||||
состоялся, операция повторяется/докатывается без потери уже сделанной работы.
|
||||
|
||||
### Вне объёма
|
||||
- Переход на внешний брокер очередей / смену схемы блокировок merge на БД-lock.
|
||||
- Полный авто-approve деплоя (ORCH-54) — отдельная задача; здесь только снятие
|
||||
технического блокера.
|
||||
- Изменение конвейера стадий (`STAGE_TRANSITIONS`) и реестра гейтов как контрактов.
|
||||
|
||||
## 5. Заинтересованные стороны
|
||||
- Owner оркестратора (self-hosting автономность).
|
||||
- Все проекты на общем инстансе (enduro-trails и пр.) — страдают от блокировки
|
||||
общей очереди.
|
||||
|
||||
## 6. Допущения и ограничения
|
||||
- `max_concurrency=1` для self-hosting сохраняется.
|
||||
- Self-hosting safety (CLAUDE.md): нельзя ронять/рестартить прод-контейнер в рамках
|
||||
задачи; нельзя пушить/форс-пушить `main`; реклейм lease не должен прерывать
|
||||
легитимно работающий merge.
|
||||
- Никаких ложных реанимаций: живой, но долгий job не должен помечаться зомби
|
||||
(нужен порог/грейс «N тиков» + проверка реальной смерти, а не просто долготы).
|
||||
- Контракт **never-raise** для всей новой фоновой логики (как у reconciler/merge_gate).
|
||||
- Kill-switch на каждый новый механизм (как `reconcile_enabled` / `merge_gate_enabled`).
|
||||
|
||||
## 7. Критерий успеха (бизнес-уровень)
|
||||
После фикса воспроизводимый сценарий «успешный deployer-проход + смерть процесса/
|
||||
self-restart» НЕ оставляет зомби-job и зажатого lease: задача либо корректно
|
||||
доезжает до `done` сама, либо откатывается по штатному контракту — **без участия
|
||||
оператора**. Регресс-тест на jobs-зомби и stale-lease зелёный.
|
||||
170
docs/work-items/ORCH-065/02-trz.md
Normal file
170
docs/work-items/ORCH-065/02-trz.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# ТЗ — ORCH-065: job-reaper + stale merge-lease reclaim + идемпотентный merge
|
||||
|
||||
Work Item ID: ORCH-065
|
||||
Базируется на: 01-brd.md
|
||||
Примечание архитектору: ТЗ фиксирует ТРЕБОВАНИЯ и кандидатные модули. Выбор
|
||||
конкретной реализации (новый модуль vs расширение reconciler; колонка `jobs.pid`
|
||||
vs эвристика на `agent_runs`) — за стадией architecture (ADR). Если какая-либо
|
||||
часть ТЗ окажется нереализуемой/спорной — вернуть в Анализ, не комментировать
|
||||
задним числом.
|
||||
|
||||
## 0. Текущее состояние (факты из кода)
|
||||
|
||||
| Место | Факт |
|
||||
|-------|------|
|
||||
| `src/queue_worker.py` `_drain_once` | claim не происходит, пока `count_running_jobs() >= max_concurrency`. Одна зомби-строка `running` при concurrency=1 блокирует всю очередь. |
|
||||
| `src/agents/launcher.py` `_monitor_agent` → `_finalize_job` | статус job (`done`/`queued`/`failed`) выставляется ТОЛЬКО в этом monitor-потоке. Смерть потока/процесса до финализации ⇒ job навсегда `running`. |
|
||||
| `src/main.py` (lifespan, строки ~55-61) | `requeue_running_jobs()` вызывается ТОЛЬКО при старте процесса. |
|
||||
| `src/db.py` `requeue_running_jobs` | flip всех `running`→`queued`. Без рестарта не запускается. |
|
||||
| `src/db.py` таблица `jobs` | колонки `pid`/`heartbeat` НЕТ; есть `run_id`, `started_at`, `attempts`, `max_attempts`. |
|
||||
| `src/merge_gate.py` `acquire_merge_lease` | реклейм stale lease (age `>= merge_lock_timeout_s`) и corrupt — ТОЛЬКО лениво в момент `acquire`. Lease пишет `pid`, но liveness по pid нигде не проверяется. |
|
||||
| `src/merge_gate.py` `release_merge_lease` | holder-aware (по `branch`), идемпотентен. Вызовы: `webhooks/gitea.py:380` (PR-merged), `stage_engine.py:352/740/876/952`, `qg/checks.py:683/691/697`. |
|
||||
| `src/qg/checks.py` `check_branch_mergeable` | при SUCCESS lease ДЕРЖИТСЯ до фактического merge PR. Если процесс умрёт после этого — lease зажат. |
|
||||
| `src/reconciler.py` | паттерн-образец фонового daemon-потока (never-raise, kill-switch, observability в `/queue`). |
|
||||
|
||||
## 1. Задействованные модули `src/`
|
||||
|
||||
- `src/db.py` — новые job-запросы для reaper (выборка stale running-job, атомарный
|
||||
reap). Возможна lightweight-миграция (см. §3).
|
||||
- **Job-reaper** — НОВЫЙ модуль (кандидат `src/job_reaper.py`) ИЛИ расширение
|
||||
`src/reconciler.py`. Решение — за архитектором; ТЗ требует daemon-поток по образцу
|
||||
`reconciler` (never-raise, `_stop`-Event, старт/стоп в `main.lifespan`, снимок в
|
||||
`/queue`).
|
||||
- `src/merge_gate.py` — функция проактивного реклейма stale/dead lease (по pid-
|
||||
liveness + по TTL); helper проверки liveness pid; helper идемпотентной
|
||||
финализации merge.
|
||||
- `src/main.py` — старт/стоп нового daemon-потока в `lifespan` (после `worker.start()`
|
||||
/ `reconciler.start()`, симметрично остановка перед `worker.stop()`); вызов
|
||||
стартового реклейма stale-lease рядом с `requeue_running_jobs()`.
|
||||
- `src/config.py` — новые настройки/флаги (см. §5).
|
||||
- `src/main.py` `GET /queue` — блок наблюдаемости reaper (образец `reconcile`/
|
||||
`post_deploy`).
|
||||
|
||||
## 2. Функциональные требования
|
||||
|
||||
### FR-1. Job-reaper (Проблема A)
|
||||
- Фоновый поток периодически (`reaper_interval_s`) сканирует строки `jobs` в статусе
|
||||
`running`.
|
||||
- Для каждого `running`-job определяет, **жив ли его исполнитель**. «Мёртвым» job
|
||||
считается, когда выполнено и устойчиво (см. FR-1.3) хотя бы одно из:
|
||||
- процесс агента (по pid/run_id) не существует, а финализация не произошла;
|
||||
- `agent_runs` строки run_id имеет `finished_at`/`exit_code` (процесс реально
|
||||
завершился), но `jobs.status` всё ещё `running` (monitor умер между записью
|
||||
exit_code и `_finalize_job`);
|
||||
- job висит `running` дольше предохранительного потолка
|
||||
`reaper_max_running_s` (заведомо больше любого легитимного `agent_timeout` +
|
||||
grace) — backstop на случай, когда liveness определить нельзя.
|
||||
- FR-1.2 Действие при подтверждённой смерти:
|
||||
- если есть достоверный успешный исход (`agent_runs.exit_code == 0`) — довести job
|
||||
к корректному завершению **через тот же контракт**, что `_finalize_job`
|
||||
(включая, при необходимости, повторную попытку auto-advance) — НЕ дублировать
|
||||
переход, если он уже произошёл (идемпотентность через `has_active_job_for_task`
|
||||
/ проверку стадии);
|
||||
- если исход неуспешный/неизвестен и бюджет попыток не исчерпан
|
||||
(`attempts < max_attempts`) — `queued` (повторная постановка), как делает
|
||||
`requeue_running_jobs`;
|
||||
- если бюджет исчерпан — `failed` + Telegram-алерт.
|
||||
- FR-1.3 **Анти-ложноположительность.** Job помечается зомби только после
|
||||
устойчивого подтверждения смерти: процесс мёртв на протяжении `reaper_dead_ticks`
|
||||
последовательных тиков (≥2) ИЛИ превышен `reaper_max_running_s`. Живой долгий
|
||||
агент (в пределах своего `agent_timeout`) НИКОГДА не реапится.
|
||||
- FR-1.4 Работает **без рестарта** процесса (главное отличие от существующего
|
||||
`requeue_running_jobs`).
|
||||
- FR-1.5 Restart-safe: после рестарта поведение корректно совмещается со стартовым
|
||||
`requeue_running_jobs()` (нет двойной обработки одной строки; атомарность reap-
|
||||
UPDATE с guard по `status='running'`, как в `claim_next_job`).
|
||||
|
||||
### FR-2. Проактивный реклейм stale/dead merge-lease (Проблема B)
|
||||
- FR-2.1 На старте процесса (рядом с `requeue_running_jobs()` в `lifespan`) и
|
||||
периодически в фоновом потоке: для каждого репо с merge-gate проверить lease и
|
||||
освободить его, если держатель **мёртв** или lease **просрочен**.
|
||||
- FR-2.2 «Держатель мёртв» = pid из lease не существует в системе (liveness-проба,
|
||||
напр. `os.kill(pid, 0)` с обработкой `ProcessLookupError`/`PermissionError`),
|
||||
при условии что pid принадлежит этому хосту/неймспейсу. «Просрочен» = `age >=
|
||||
merge_lock_timeout_s` (существующий TTL-контракт сохраняется).
|
||||
- FR-2.3 Реклейм **holder-aware и безопасен**: НЕ освобождать lease, чей держатель
|
||||
жив и в пределах TTL (защита легитимного merge). Логировать `warning` при каждом
|
||||
реклейме (наблюдаемость, как сейчас в `acquire_merge_lease`).
|
||||
- FR-2.4 Условность как ORCH-35/43: реально только для self-hosting/`merge_gate_repos`;
|
||||
прочие репо — no-op.
|
||||
- FR-2.5 Контракт **never-raise**; любая ошибка реклейма не должна валить поток.
|
||||
|
||||
### FR-3. Идемпотентная финализация merge (Проблема C)
|
||||
- FR-3.1 Если ветка прошла rebase+re-test (догнана до `origin/main` и зелёная), но
|
||||
merge PR не состоялся из-за смерти процесса — система должна **докатить/повторить**
|
||||
merge без повторного прогона дорогих шагов, когда это безопасно.
|
||||
- FR-3.2 Финализация merge должна быть **идемпотентной**: повторный вызов при уже
|
||||
слитом PR — no-op (определять по состоянию PR в Gitea и/или по
|
||||
`branch_is_behind_main`/состоянию `main`), без ошибки и без второго слияния.
|
||||
- FR-3.3 Восстановление re-drive обеспечивается штатными механизмами (reaper
|
||||
довёл job до `queued` → повторный проход стадии `deploy`/merge-gate; либо
|
||||
reconciler доигрывает переход). Дублирующая логика merge НЕ создаётся — переиспользуются
|
||||
существующие пути (`check_branch_mergeable` / deployer-merge).
|
||||
- FR-3.4 При повторе lease берётся заново (идемпотентный re-acquire «held by self»
|
||||
по branch уже поддержан в `acquire_merge_lease`).
|
||||
|
||||
### FR-4. Наблюдаемость
|
||||
- FR-4.1 Блок `reaper` в `GET /queue`: enabled, interval, last_run_ts, reaped_total,
|
||||
last_reaped (job_id/agent), lease_reclaimed_total (best-effort, как у reconciler).
|
||||
- FR-4.2 Каждый reap и каждый lease-reclaim — `logger.warning` с идентификаторами
|
||||
(job_id, run_id, pid, repo, branch).
|
||||
- FR-4.3 При reap→`failed` и при lease-reclaim — Telegram (как существующие алерты).
|
||||
|
||||
## 3. Изменения схемы БД
|
||||
- Текущая `jobs` НЕ содержит `pid`. Для надёжной pid-liveness job-reaper'у, скорее
|
||||
всего, потребуется **lightweight-миграция**: добавить `jobs.pid INTEGER` (через
|
||||
`_ensure_column`, идемпотентно, безопасно на live prod DB — паттерн уже
|
||||
применяется в `db.py`). pid проставляется в `_spawn` рядом с `run_id`/`started_at`.
|
||||
- **Альтернатива без миграции** (на усмотрение архитектора): определять смерть по
|
||||
`agent_runs.finished_at/exit_code` + потолку `reaper_max_running_s`, без хранения
|
||||
pid в `jobs`. ADR должен зафиксировать выбор и обоснование.
|
||||
- Реестры `STAGE_TRANSITIONS` и `QG_CHECKS` — **без изменений** (новых стадий/гейтов
|
||||
не вводим; reaper и lease-reclaim — фоновые механизмы, не стадии).
|
||||
- Merge-lease остаётся файловым (`.merge-lease-<repo>.json`); схема файла lease
|
||||
не меняется (pid и acquired_at уже есть).
|
||||
|
||||
## 4. Изменения API
|
||||
- `GET /queue` — добавить блок `reaper` (read-only наблюдаемость). Прочие endpoints
|
||||
без изменений. Новых webhook-роутов нет.
|
||||
|
||||
## 5. Конфигурация / kill-switches (`src/config.py`)
|
||||
Именование — по образцу `reconcile_*` / `merge_*`. Кандидаты (точные имена/дефолты
|
||||
уточняет архитектор):
|
||||
|
||||
| Настройка | Назначение | Дефолт (предложение) |
|
||||
|-----------|-----------|----------------------|
|
||||
| `reaper_enabled` | глобальный kill-switch job-reaper | `true` |
|
||||
| `reaper_interval_s` | период сканирования | `60` |
|
||||
| `reaper_dead_ticks` | сколько подряд тиков pid должен быть мёртв перед reap | `2` |
|
||||
| `reaper_max_running_s` | потолок «running» (backstop), > max agent_timeout+grace | `3600` |
|
||||
| `lease_reclaim_enabled` | kill-switch проактивного реклейма lease | `true` |
|
||||
| (переиспользуется) `merge_lock_timeout_s` | TTL lease | `300` (как есть) |
|
||||
| (переиспользуется) `merge_gate_repos` | область применения lease-reclaim | как есть |
|
||||
|
||||
Все флаги — пробрасываются из env (`ORCH_*`), `false` → строго прежнее поведение.
|
||||
|
||||
## 6. Требования к QG checks
|
||||
- Новых QG checks НЕ вводить (это фоновые resilience-механизмы, не гейты выхода со
|
||||
стадии). `check_branch_mergeable` остаётся контрактно неизменным; допускается лишь
|
||||
переиспользование его как идемпотентного пути финализации merge (FR-3.3).
|
||||
|
||||
## 7. Артефакты pipeline, создаваемые/обновляемые в ЭТОМ PR
|
||||
- Код: см. §1.
|
||||
- `06-adr/ADR-001-*.md` — архитектурное решение (где живёт reaper; pid-колонка vs
|
||||
эвристика; механизм идемпотентного merge) — создаёт architect.
|
||||
- `docs/architecture/README.md` — новый раздел про job-reaper + lease-reclaim
|
||||
(golden-source, в этом же PR).
|
||||
- `docs/architecture/internals.md` — детали (если затрагивается схема БД / потоки).
|
||||
- `CHANGELOG.md` — запись ORCH-065.
|
||||
- `.env.example` — новые `ORCH_*` флаги (канон секретов/настроек).
|
||||
- `docs/operations/INFRA.md` — упоминание поведения при self-restart, если
|
||||
затрагивается (best-effort).
|
||||
|
||||
## 8. Инварианты (НЕ нарушать)
|
||||
- Не ронять/не рестартить прод-контейнер `orchestrator` в рамках задачи.
|
||||
- Никогда не пушить/форс-пушить `main`; реклейм lease не инициирует git-операций.
|
||||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, контракты `check_*`, БАГ-8 откат,
|
||||
exit-коды deploy-хука — без изменений.
|
||||
- never-raise на единицу фоновой работы; идемпотентность; restart-safe; тишина при
|
||||
отсутствии аномалий (как reconciler).
|
||||
- Анти-ложноположительность (FR-1.3): живой долгий агент не реапится.
|
||||
122
docs/work-items/ORCH-065/03-acceptance-criteria.md
Normal file
122
docs/work-items/ORCH-065/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Критерии приёмки — ORCH-065
|
||||
|
||||
Work Item ID: ORCH-065
|
||||
Формат: каждый критерий имеет явное условие PASS/FAIL. Все критерии должны быть
|
||||
PASS для прохождения review/testing.
|
||||
|
||||
## A. Job-reaper (Проблема A)
|
||||
|
||||
### AC-1 — реап мёртвого running-job без рестарта
|
||||
- PASS: при наличии строки `jobs` в статусе `running`, чей процесс/исполнитель
|
||||
достоверно мёртв (pid не существует ИЛИ `agent_runs.exit_code` записан, а job всё
|
||||
ещё `running`) и условие устойчивости (FR-1.3) выполнено, фоновый reaper переводит
|
||||
строку в корректный статус (`done`/`queued`/`failed`) **без перезапуска процесса**.
|
||||
- FAIL: строка остаётся `running` после `reaper_dead_ticks` тиков / превышения
|
||||
`reaper_max_running_s`.
|
||||
|
||||
### AC-2 — разблокировка очереди при concurrency=1
|
||||
- PASS: после реапа зомби-строки `count_running_jobs()` снижается, и следующий
|
||||
queued-job успешно claim'ится воркером.
|
||||
- FAIL: очередь остаётся заблокированной зомби-строкой.
|
||||
|
||||
### AC-3 — анти-ложноположительность (живой долгий агент не реапится)
|
||||
- PASS: `running`-job с ЖИВЫМ процессом в пределах его `agent_timeout` НЕ помечается
|
||||
зомби (ни по одному тику, ни в пределах `reaper_max_running_s`, если потолок
|
||||
больше таймаута).
|
||||
- FAIL: живой агент помечен `failed`/`queued` reaper'ом.
|
||||
|
||||
### AC-4 — корректный исход по результату
|
||||
- PASS: при `agent_runs.exit_code == 0` reaper доводит до успешного завершения без
|
||||
дублирования уже выполненного stage-advance (идемпотентно); при неуспехе и
|
||||
`attempts < max_attempts` → `queued`; при исчерпании → `failed` + Telegram.
|
||||
- FAIL: успешный исход помечен `failed`; либо дублируется stage-переход; либо
|
||||
исчерпанный бюджет молча зацикливается на `queued`.
|
||||
|
||||
### AC-5 — restart-safe совместимость
|
||||
- PASS: одновременная работа стартового `requeue_running_jobs()` и периодического
|
||||
reaper не приводит к двойной обработке одной строки (атомарный UPDATE с guard
|
||||
`status='running'`).
|
||||
- FAIL: одна строка обработана дважды / гонка приводит к рассинхрону статуса.
|
||||
|
||||
## B. Stale/dead merge-lease reclaim (Проблема B)
|
||||
|
||||
### AC-6 — реклейм lease мёртвого держателя
|
||||
- PASS: lease `.merge-lease-<repo>.json`, чей `pid` не существует, проактивно
|
||||
освобождается на старте И периодическим потоком (не дожидаясь TTL и не дожидаясь
|
||||
чужого `acquire`).
|
||||
- FAIL: lease мёртвого держателя остаётся до истечения `merge_lock_timeout_s` или
|
||||
до следующего чужого `acquire`.
|
||||
|
||||
### AC-7 — реклейм по TTL сохранён
|
||||
- PASS: lease старше `merge_lock_timeout_s` освобождается (существующий контракт не
|
||||
сломан), с `logger.warning`.
|
||||
- FAIL: просроченный lease не освобождается.
|
||||
|
||||
### AC-8 — не трогать живой lease
|
||||
- PASS: lease с ЖИВЫМ держателем (pid жив) и возрастом `< merge_lock_timeout_s` НЕ
|
||||
освобождается (защита легитимного merge).
|
||||
- FAIL: освобождён lease живого держателя → возможен параллельный конфликтный merge.
|
||||
|
||||
### AC-9 — условность и never-raise
|
||||
- PASS: реклейм реален только для `merge_gate_repos`/self-hosting; для прочих репо
|
||||
— no-op; любая ошибка реклейма логируется и не валит поток (never-raise).
|
||||
- FAIL: реклейм выполняется для не-self-hosting репо; либо ошибка пробрасывается
|
||||
наружу/роняет поток.
|
||||
|
||||
## C. Идемпотентная финализация merge (Проблема C)
|
||||
|
||||
### AC-10 — докатывание незавершённого merge
|
||||
- PASS: сценарий «rebase+re-test зелёные, merge не состоялся, процесс умер»
|
||||
восстанавливается автоматически (job → `queued` reaper'ом / reconciler доигрывает),
|
||||
и merge доводится без повторного ненужного прогона дорогих шагов.
|
||||
- FAIL: задача остаётся в полу-выполненном состоянии, требует ручного merge.
|
||||
|
||||
### AC-11 — идемпотентность при уже слитом PR
|
||||
- PASS: повторный вызов финализации при уже слитом PR — no-op (определяется по
|
||||
состоянию PR/`main`), без ошибки и без второго merge.
|
||||
- FAIL: второй merge / ошибка при уже слитом PR.
|
||||
|
||||
## D. Инварианты и безопасность self-hosting
|
||||
|
||||
### AC-12 — прод-контейнер не трогается
|
||||
- PASS: ни reaper, ни lease-reclaim не рестартят/не роняют прод-контейнер и не
|
||||
инициируют git-push в `main`.
|
||||
- FAIL: любая из новых веток кода рестартит self / пушит main.
|
||||
|
||||
### AC-13 — контракты неизменны
|
||||
- PASS: `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, сигнатуры/поведение `check_*`,
|
||||
БАГ-8 откат, exit-коды deploy-хука — без изменений; новых QG checks/стадий нет.
|
||||
- FAIL: затронут любой из перечисленных контрактов.
|
||||
|
||||
### AC-14 — kill-switches
|
||||
- PASS: `reaper_enabled=false` → reaper не работает (строго прежнее поведение);
|
||||
`lease_reclaim_enabled=false` → проактивный реклейм отключён (остаётся лишь
|
||||
прежний ленивый TTL-реклейм в `acquire`).
|
||||
- FAIL: флаг `false` не отключает соответствующий механизм.
|
||||
|
||||
## E. Наблюдаемость
|
||||
|
||||
### AC-15 — блок reaper в /queue
|
||||
- PASS: `GET /queue` содержит блок `reaper` (enabled, interval, last_run_ts,
|
||||
reaped_total, last_reaped, lease_reclaimed_total).
|
||||
- FAIL: блок отсутствует/не обновляется.
|
||||
|
||||
### AC-16 — логи и алерты
|
||||
- PASS: каждый reap и lease-reclaim → `logger.warning` с идентификаторами;
|
||||
reap→`failed` и lease-reclaim → Telegram.
|
||||
- FAIL: реап/реклейм происходят молча.
|
||||
|
||||
## F. Документация (gate reviewer)
|
||||
|
||||
### AC-17 — golden-source обновлён в этом же PR
|
||||
- PASS: обновлены `docs/architecture/README.md` (раздел про reaper + lease-reclaim),
|
||||
`CHANGELOG.md`, `.env.example` (новые `ORCH_*` флаги); заведён
|
||||
`06-adr/ADR-001-*.md`.
|
||||
- FAIL: код изменён, документация — нет (reviewer → REQUEST_CHANGES).
|
||||
|
||||
## G. Тесты
|
||||
|
||||
### AC-18 — регресс-тесты зелёные
|
||||
- PASS: новые unit/integration тесты (см. 04-test-plan.yaml) проходят; существующий
|
||||
`pytest tests/ -q` зелёный (нет регресса merge_gate / queue / reconciler).
|
||||
- FAIL: любой тест из плана красный или сломан существующий тест.
|
||||
196
docs/work-items/ORCH-065/04-test-plan.yaml
Normal file
196
docs/work-items/ORCH-065/04-test-plan.yaml
Normal file
@@ -0,0 +1,196 @@
|
||||
work_item: ORCH-065
|
||||
description: >
|
||||
Тест-план для фикса zombie jobs (job-reaper), залипшего merge-lease
|
||||
(проактивный реклейм dead/stale lease) и идемпотентной финализации merge.
|
||||
Все новые фоновые механизмы — never-raise, restart-safe, kill-switch.
|
||||
Модуль reaper и точные имена функций уточнит архитектор; в module указаны
|
||||
кандидатные пути (tests/test_job_reaper.py / tests/test_merge_lease_reclaim.py).
|
||||
|
||||
tests:
|
||||
# ---- A. Job-reaper ----
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: >
|
||||
Reaper переводит running-job с мёртвым исполнителем в корректный статус
|
||||
без рестарта процесса (pid не существует / exit_code записан, job всё ещё
|
||||
running). Покрывает AC-1.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: >
|
||||
Анти-ложноположительность: running-job с ЖИВЫМ процессом в пределах
|
||||
agent_timeout НЕ реапится (ни по одному тику, ни в пределах reaper_max_running_s).
|
||||
Покрывает AC-3.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: >
|
||||
Устойчивость: job помечается зомби только после reaper_dead_ticks
|
||||
последовательных тиков смерти pid (>=2), а не на первом тике. Покрывает FR-1.3.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: >
|
||||
Backstop по потолку: job, висящий running дольше reaper_max_running_s,
|
||||
реапится даже если liveness определить нельзя. Покрывает FR-1.1/AC-1.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: >
|
||||
Корректный исход: exit_code==0 -> успешное завершение без дублирования
|
||||
stage-advance; неуспех при attempts<max -> queued; исчерпан бюджет -> failed
|
||||
+ Telegram. Покрывает AC-4.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: >
|
||||
Атомарность reap-UPDATE с guard status='running': параллельная обработка
|
||||
одной строки (стартовый requeue_running_jobs + reaper) не приводит к двойному
|
||||
reap. Покрывает AC-5.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: >
|
||||
Kill-switch reaper_enabled=false -> reaper не трогает ни одной строки
|
||||
(строго прежнее поведение). Покрывает AC-14.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: >
|
||||
never-raise: ошибка БД/ОС внутри одного тика reaper не пробрасывается и не
|
||||
останавливает поток (изоляция per-job, образец reconciler). Покрывает AC-9.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: integration
|
||||
description: >
|
||||
Очередь разблокируется: после reap зомби-строки при max_concurrency=1
|
||||
следующий queued-job успешно claim'ится (claim_next_job + count_running_jobs).
|
||||
Покрывает AC-2.
|
||||
module: tests/test_queue.py
|
||||
expected: PASS
|
||||
|
||||
# ---- B. Stale/dead merge-lease reclaim ----
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: >
|
||||
Реклейм lease с мёртвым pid (os.kill(pid,0) -> ProcessLookupError)
|
||||
проактивно, не дожидаясь TTL и чужого acquire. Покрывает AC-6.
|
||||
module: tests/test_merge_lease_reclaim.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: >
|
||||
Реклейм по TTL (age >= merge_lock_timeout_s) сохранён, с logger.warning.
|
||||
Покрывает AC-7. (расширяет существующий stale-lease сценарий merge_gate.)
|
||||
module: tests/test_merge_lease_reclaim.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: >
|
||||
Живой lease (pid жив, age < TTL) НЕ освобождается — защита легитимного merge.
|
||||
Покрывает AC-8.
|
||||
module: tests/test_merge_lease_reclaim.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-13
|
||||
type: unit
|
||||
description: >
|
||||
Условность: реклейм реален только для merge_gate_repos/self-hosting; для
|
||||
прочих репо no-op. Покрывает AC-9.
|
||||
module: tests/test_merge_lease_reclaim.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-14
|
||||
type: unit
|
||||
description: >
|
||||
never-raise: ошибка чтения/удаления lease-файла не валит реклейм-поток.
|
||||
Покрывает AC-9.
|
||||
module: tests/test_merge_lease_reclaim.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-15
|
||||
type: unit
|
||||
description: >
|
||||
Kill-switch lease_reclaim_enabled=false -> проактивный реклейм отключён,
|
||||
остаётся лишь прежний ленивый TTL-реклейм в acquire_merge_lease.
|
||||
Покрывает AC-14.
|
||||
module: tests/test_merge_lease_reclaim.py
|
||||
expected: PASS
|
||||
|
||||
# ---- C. Идемпотентная финализация merge ----
|
||||
- id: TC-16
|
||||
type: unit
|
||||
description: >
|
||||
Идемпотентность финализации: повторный вызов при уже слитом PR / уже
|
||||
актуальном main (branch_is_behind_main == False) — no-op, без ошибки и без
|
||||
второго merge. Покрывает AC-11.
|
||||
module: tests/test_merge_gate.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-17
|
||||
type: integration
|
||||
description: >
|
||||
Восстановление: сценарий "rebase+re-test зелёные, merge не состоялся,
|
||||
процесс умер" -> job доводится до queued reaper'ом и merge докатывается
|
||||
штатным путём без повторного дорогого re-test, когда безопасно. Покрывает AC-10.
|
||||
module: tests/test_merge_gate_race.py
|
||||
expected: PASS
|
||||
|
||||
# ---- D/E. Инварианты и наблюдаемость ----
|
||||
- id: TC-18
|
||||
type: integration
|
||||
description: >
|
||||
GET /queue содержит блок reaper (enabled, interval, last_run_ts,
|
||||
reaped_total, last_reaped, lease_reclaimed_total). Покрывает AC-15.
|
||||
module: tests/test_queue.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-19
|
||||
type: unit
|
||||
description: >
|
||||
Контракты неизменны: STAGE_TRANSITIONS и реестр QG_CHECKS не получили новых
|
||||
стадий/чеков; check_branch_mergeable сигнатурно не изменён. Покрывает AC-13.
|
||||
module: tests/test_config.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-20
|
||||
type: unit
|
||||
description: >
|
||||
Новые настройки reaper_*/lease_reclaim_* присутствуют в config с дефолтами и
|
||||
читаются из ORCH_* env. Покрывает §5 ТЗ / AC-14.
|
||||
module: tests/test_config.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-21
|
||||
type: unit
|
||||
description: >
|
||||
Стартовый реклейм stale/dead lease вызывается в lifespan рядом с
|
||||
requeue_running_jobs (smoke на регистрацию старт/стоп reaper-потока).
|
||||
Покрывает FR-2.1 / AC-6.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
regression:
|
||||
- command: pytest tests/ -q
|
||||
expected: PASS
|
||||
note: >
|
||||
Полный прогон не должен ломать существующие тесты merge_gate / queue /
|
||||
reconciler / deploy.
|
||||
@@ -0,0 +1,275 @@
|
||||
# ADR-001 (ORCH-065): Job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge
|
||||
|
||||
## Статус
|
||||
Accepted — 2026-06-07
|
||||
|
||||
Связь со сквозным ADR: [docs/architecture/adr/adr-0011-job-reaper-lease-reclaim.md](../../../architecture/adr/adr-0011-job-reaper-lease-reclaim.md).
|
||||
|
||||
## Контекст
|
||||
|
||||
Оркестратор — единый инстанс с **общей БД и общей очередью** (`jobs`,
|
||||
`max_concurrency=1` для self-hosting). BRD/ТЗ фиксируют три связанных класса
|
||||
отказов «процесс/поток умер, а состояние осталось захваченным навсегда»:
|
||||
|
||||
- **A — zombie jobs.** Статус job (`done`/`queued`/`failed`) выставляется ТОЛЬКО
|
||||
в `launcher._monitor_agent` → `_finalize_job` внутри того же процесса. Смерть
|
||||
monitor-потока/процесса между `proc.wait()` и `_finalize_job` (краш, OOM,
|
||||
self-restart во время deploy) оставляет строку `jobs` навсегда `running`.
|
||||
Единственная защита — `requeue_running_jobs()`, срабатывает ТОЛЬКО на старте
|
||||
процесса. Зомби без рестарта не реанимируется никогда. При `max_concurrency=1`
|
||||
одна зомби-строка блокирует claim всех job (`count_running_jobs() >=
|
||||
max_concurrency`) → встаёт конвейер ВСЕХ проектов. Доказано: jobs 236/239/242/254
|
||||
(07.06).
|
||||
- **B — залипший merge-lease.** Файловый lease `.merge-lease-<repo>.json`
|
||||
(ORCH-043) реклеймится **лениво и только по возрасту** (`age >=
|
||||
merge_lock_timeout_s`) и **только** в момент `acquire_merge_lease` другой
|
||||
задачей. Liveness держателя по pid не проверяется, хотя pid в lease пишется.
|
||||
Смерть с зажатым lease блокирует чужие merge до истечения TTL.
|
||||
- **C — неидемпотентная финализация merge.** Если rebase+re-test зелёные, но
|
||||
процесс умер до фактического merge PR — повторного докатывания нет; дорогая
|
||||
работа (rebase+re-test) сделана, а задача висит.
|
||||
|
||||
Факты кода, на которых строится решение:
|
||||
- `_spawn` (launcher.py:401) создаёт `subprocess.Popen(["bash","-c",cmd])`;
|
||||
`proc.pid` — pid агентского процесса, дочернего к процессу оркестратора в ОДНОМ
|
||||
pid-namespace контейнера. Сейчас `jobs.pid` НЕ хранится.
|
||||
- `_monitor_agent` (launcher.py:541) порядок: `proc.wait()` → запись
|
||||
`agent_runs.finished_at/exit_code` → git commit/push (+PR) → БАГ-8 deployer
|
||||
rollback → usage-комменты → `_try_advance_stage` (exit0, gate-driven advance)
|
||||
→ `_finalize_job` (драйв статуса job по контракту attempts/transient).
|
||||
- `claim_next_job` (db.py:454) — атомарный claim через `UPDATE ... WHERE id=? AND
|
||||
status='queued'` + `rowcount` (образец атомарности).
|
||||
- `reconciler.py` — образец фонового daemon-потока (never-raise, `_stop`-Event,
|
||||
старт/стоп в `main.lifespan`, снимок в `/queue`, kill-switch).
|
||||
- `merge_gate.py`: lease пишет `pid=os.getpid()` (pid процесса оркестратора, НЕ
|
||||
агента), `acquired_at`; `release_merge_lease` уже holder-aware (по `branch`) и
|
||||
идемпотентен; `acquire_merge_lease` идемпотентен для «held by self» (по branch).
|
||||
- `is_self_hosting_repo` / `merge_gate_repos` — образец условности (ORCH-35/43).
|
||||
|
||||
## Решение
|
||||
|
||||
### Р-1. Job-reaper — отдельный daemon-поток `src/job_reaper.py`
|
||||
|
||||
Reaper — **новый модуль и отдельный daemon-поток** (НЕ расширение reconciler).
|
||||
Обоснование: reconciler работает на уровне **stage-transition** (источник истины —
|
||||
гейт/Plane); reaper работает на уровне **jobs/agent_runs** (источник истины —
|
||||
liveness процесса). Это разные never-raise-домены и разные kill-switch'и; слияние
|
||||
в один тик смешало бы ответственности. Reaper копирует проверенный каркас
|
||||
`Reconciler`: `threading.Thread(daemon=True)` + `threading.Event`, старт/стоп в
|
||||
`main.lifespan`, снимок в `/queue`, per-job изоляция исключений.
|
||||
|
||||
**Liveness — трёхуровневая (defense in depth):**
|
||||
|
||||
1. **Tier-1 (liveness, основной): мёртвый pid.** Добавляем колонку `jobs.pid`
|
||||
(см. Р-4). В `_spawn` рядом с `run_id`/`started_at` пишем `proc.pid`. Reaper:
|
||||
`pid_alive(pid)` = `os.kill(pid, 0)` с обработкой `ProcessLookupError` (мёртв)
|
||||
/ `PermissionError` (жив, чужой) — единственный сигнал, ловящий «monitor умер
|
||||
ДО записи `finished_at`».
|
||||
2. **Tier-2 (completion race): exit_code записан, job ещё `running`.** Окно
|
||||
**неоднозначно**: это И «monitor умер между записью exit_code и
|
||||
`_finalize_job`», И «живой monitor ещё финализирует» — `_monitor_agent`
|
||||
пишет `exit_code` ПЕРВЫМ, затем git commit/push (+PR), БАГ-8-проверку и
|
||||
сетевые usage-комментарии в Plane (секунды-десятки секунд), и лишь потом
|
||||
`_try_advance_stage` → `_finalize_job`. pid агента к этому моменту уже мёртв в
|
||||
ОБОИХ случаях, поэтому по pid их не различить. **Анти-ложноположительность
|
||||
Tier-2 (FR-1.3, AC-3): finalization-grace.** Job реапится по Tier-2 только
|
||||
когда `exit_code` записан не меньше `reaper_finalize_grace_s` назад (потолок
|
||||
заведомо > максимального окна финализации). В пределах grace строка не
|
||||
трогается (живой финализирующий monitor НИКОГДА не реапится; нет дубль-advance
|
||||
/ дубль-enqueue). После grace monitor заведомо мёртв → исход **известен**.
|
||||
3. **Tier-3 (backstop по потолку):** job висит `running` дольше
|
||||
`reaper_max_running_s` (заведомо > max `agent_timeout`+grace). Реап даже когда
|
||||
liveness определить нельзя (pid переиспользован/неизвестен).
|
||||
|
||||
**Анти-ложноположительность (FR-1.3, AC-3):** по Tier-1 job реапится только после
|
||||
`reaper_dead_ticks` (≥2) ПОДРЯД тиков мёртвого pid — in-memory streak-счётчик по
|
||||
`job_id` (best-effort, сбрасывается на рестарте — но рестарт покрыт стартовым
|
||||
`requeue_running_jobs`). Tier-3 — одношаговый (порог времени, streak не нужен).
|
||||
Живой агент в пределах своего `agent_timeout` НЕ реапится никогда (pid жив + не
|
||||
превышен потолок).
|
||||
|
||||
**Действие при подтверждённой смерти (FR-1.2, AC-4) — переиспользование
|
||||
существующих контрактов, без дублирования:**
|
||||
|
||||
- **Атомарный reap-claim.** Перед любым действием с побочными эффектами reaper
|
||||
атомарно «застолбляет» строку тем же приёмом, что `claim_next_job`: терминальный
|
||||
flip несёт guard `WHERE id=? AND status='running'` и проверяет `rowcount`. При
|
||||
гонке (поздно доехавший monitor, стартовый `requeue_running_jobs`) проигравший
|
||||
видит `rowcount==0` и НЕ обрабатывает строку повторно (AC-5).
|
||||
- **Исход известен (Tier-2, exit_code в `agent_runs`, grace прошёл):**
|
||||
- `exit==0`: **claim-BEFORE-act, gate-driven idempotent advance.** Порядок
|
||||
критичен (см. «Атомарный reap-claim» выше): атомарный claim ОБЯЗАН
|
||||
предшествовать любому `advance_stage`/`enqueue_job`. Поскольку claim
|
||||
переводит строку ИЗ `running`, прогнать advance ДО claim, чтобы узнать цвет
|
||||
гейта, нельзя — поэтому канонический QG оценивается **read-only, без
|
||||
побочных эффектов** (тот же `_run_qg`, что у reconciler) ПЕРЕД claim:
|
||||
- стадия уже продвинута мимо этого агента → атомарный `done` без advance
|
||||
(идемпотентная уборка);
|
||||
- гейт зелёный → атомарный claim `done` ПЕРВЫМ, и только победитель claim
|
||||
выполняет `_try_advance_stage` (advance + `enqueue_job` следующей стадии)
|
||||
РОВНО один раз; проигравший claim (поздний monitor / стартовый
|
||||
`requeue_running_jobs`) НЕ делает побочных эффектов (нет дубль-advance /
|
||||
дубль-enqueue);
|
||||
- гейт красный (monitor умер ДО git-push, артефакта нет) → НЕ выдумываем
|
||||
`done`, уходим в ветку «исход неуспешен» ниже.
|
||||
**Источник истины — гейт, не «exit0».**
|
||||
- `exit!=0`: ровно существующий контракт `_finalize_job` (классификация
|
||||
transient/permanent, `attempts<max` → `queued`, иначе `failed`+Telegram).
|
||||
- **Исход неизвестен (Tier-1 мёртвый pid без exit_code, или Tier-3 backstop):**
|
||||
не выдумываем `exit0`. Если `attempts < max_attempts` → `queued` (как
|
||||
`requeue_running_jobs`); если бюджет исчерпан → `failed` + Telegram-алерт.
|
||||
|
||||
**Restart-safe (FR-1.5, AC-5):** reaper стартует в `lifespan` ПОСЛЕ
|
||||
`requeue_running_jobs()`, поэтому при рестарте сначала отрабатывает стартовый
|
||||
requeue, а периодический reaper лишь добивает то, что возникнет позже; атомарный
|
||||
guard `status='running'` исключает двойную обработку.
|
||||
|
||||
### Р-2. Проактивный реклейм stale/dead merge-lease — функции в `merge_gate.py`
|
||||
|
||||
Логика lease живёт в одном месте (`merge_gate.py`). Добавляем:
|
||||
- `pid_alive(pid) -> bool` (never-raise; ошибка/`PermissionError` → считаем
|
||||
«жив», т.е. консервативно НЕ реклеймим — безопаснее не трогать).
|
||||
- `reclaim_stale_lease(repo) -> bool` — для репо из области (см. ниже): прочитать
|
||||
lease; освободить (`release_merge_lease(repo, branch=holder_branch)` —
|
||||
holder-aware), если держатель **мёртв** (`pid` из lease не жив) ИЛИ **просрочен**
|
||||
(`age >= merge_lock_timeout_s`). Живой держатель в пределах TTL — НЕ трогать
|
||||
(AC-8, защита легитимного merge). Каждый реклейм → `logger.warning` +
|
||||
Telegram.
|
||||
|
||||
**Точки вызова (FR-2.1):**
|
||||
- на старте — в `lifespan` рядом с `requeue_running_jobs()`;
|
||||
- периодически — из тика reaper (один общий фоновый поток на оба механизма A и B).
|
||||
|
||||
**Условность (FR-2.4, AC-9):** реально только для `merge_gate_repos`/self-hosting
|
||||
(тот же предикат, что merge-gate); прочие репо — no-op. Kill-switch
|
||||
`lease_reclaim_enabled` (=false → остаётся лишь прежний ленивый TTL-реклейм в
|
||||
`acquire_merge_lease`). Контракт **never-raise**: ошибка реклейма логируется и не
|
||||
валит поток.
|
||||
|
||||
**pid-семантика lease:** lease пишет pid процесса ОРКЕСТРАТОРА (`os.getpid()`).
|
||||
После рестарта контейнера старый pid мёртв → детектируется. Риск pid-reuse
|
||||
(контейнер мог переиспользовать номер pid) закрыт тем, что реклейм срабатывает по
|
||||
**ИЛИ** (pid мёртв **ИЛИ** TTL истёк): даже при ложном «жив» TTL добьёт lease
|
||||
(контракт ORCH-043 сохранён). См. 10-tech-risks.
|
||||
|
||||
### Р-3. Идемпотентная финализация merge (Проблема C) — re-drive + guard, без новой merge-логики
|
||||
|
||||
Per FR-3.3 — НЕ создаём дублирующую merge-логику. Восстановление обеспечивается
|
||||
**штатными путями**:
|
||||
- reaper доводит зомби-job до `queued` → стадия `deploy-staging`/`deploy`
|
||||
переисполняется и снова проходит `check_branch_mergeable` (merge-gate), ЛИБО
|
||||
reconciler доигрывает переход по зелёному гейту;
|
||||
- дорогие шаги не повторяются «вхолостую»: `branch_is_behind_main == False` → этап
|
||||
rebase+re-test пропускается (ветка уже догнана);
|
||||
- lease при повторе берётся заново — `acquire_merge_lease` уже идемпотентен для
|
||||
«held by self» по branch (FR-3.4).
|
||||
|
||||
**Идемпотентность у самого merge (FR-3.2, AC-11):** добавляем детерминированный
|
||||
never-raise guard `pr_already_merged(repo, branch) -> bool` (переиспользует
|
||||
существующий Gitea-клиент; запрос состояния PR). Путь слияния (deployer/merge)
|
||||
консультируется с этим guard ПЕРЕД повторным merge: PR уже слит → no-op (без
|
||||
второго merge и без ошибки). Это единственная новая «merge-related» функция — она
|
||||
не сливает, а лишь читает состояние, поэтому не нарушает «no duplicate merge
|
||||
logic».
|
||||
|
||||
### Р-4. Изменение схемы БД — `jobs.pid INTEGER` (lightweight migration)
|
||||
|
||||
Колонка добавляется идемпотентно через существующий `_ensure_column(conn, "jobs",
|
||||
"pid", "INTEGER")` в `init_db` (паттерн уже применяется к `jobs.transient_attempts`
|
||||
/ `jobs.available_at` — безопасно на live prod DB). `pid` проставляется в `_spawn`
|
||||
рядом с `run_id`/`started_at`. **Альтернатива без миграции отвергнута** (см.
|
||||
Альтернативы): только по `agent_runs.finished_at/exit_code` нельзя поймать
|
||||
зомби, у которого monitor умер ДО записи exit_code — а это и есть основной класс
|
||||
инцидента. `STAGE_TRANSITIONS`, `QG_CHECKS`, схема `agent_runs`, файл-схема lease —
|
||||
без изменений.
|
||||
|
||||
### Р-5. Конфигурация (`src/config.py`, env `ORCH_*`)
|
||||
|
||||
| Настройка | Назначение | Дефолт |
|
||||
|-----------|-----------|--------|
|
||||
| `reaper_enabled` | глобальный kill-switch job-reaper | `True` |
|
||||
| `reaper_interval_s` | период сканирования | `60` |
|
||||
| `reaper_dead_ticks` | подряд тиков мёртвого pid перед реапом (Tier-1) | `2` |
|
||||
| `reaper_max_running_s` | потолок `running` (Tier-3 backstop), > max agent_timeout+grace | `3600` |
|
||||
| `reaper_finalize_grace_s` | Tier-2 grace: сколько `exit_code` должен быть записан до реапа (> max окна финализации) | `300` |
|
||||
| `lease_reclaim_enabled` | kill-switch проактивного реклейма lease | `True` |
|
||||
| (reuse) `merge_lock_timeout_s` | TTL lease | `300` |
|
||||
| (reuse) `merge_gate_repos` | область применения lease-reclaim | как есть |
|
||||
|
||||
`false` → строго прежнее поведение (AC-14).
|
||||
|
||||
### Р-6. Наблюдаемость (`GET /queue`)
|
||||
|
||||
Блок `reaper` (образец `reconcile`/`post_deploy`): `enabled`, `interval`,
|
||||
`last_run_ts`, `reaped_total`, `last_reaped` (`{job_id, agent, outcome}`),
|
||||
`lease_reclaimed_total`. Каждый reap и lease-reclaim → `logger.warning` с
|
||||
идентификаторами (`job_id`, `run_id`, `pid`, `repo`, `branch`). reap→`failed` и
|
||||
lease-reclaim → Telegram (AC-16).
|
||||
|
||||
### Р-7. Lifespan (`src/main.py`)
|
||||
|
||||
Старт (после существующего `requeue_running_jobs()`):
|
||||
```
|
||||
init_db() # + _ensure_column(jobs, pid)
|
||||
... orphan-recovery (M-1) ...
|
||||
requeue_running_jobs()
|
||||
+ startup lease-reclaim # reclaim_stale_lease для merge_gate_repos
|
||||
worker.start()
|
||||
reconciler.start()
|
||||
+ reaper.start() # НОВЫЙ daemon-поток (A + периодический B)
|
||||
```
|
||||
Стоп (reverse): `reaper.stop()` → `reconciler.stop()` → `worker.stop()`.
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **Reaper как часть reconciler** — отвергнуто: смешивает stage-уровень и
|
||||
jobs-уровень, два разных kill-switch'а в одном тике, хуже изоляция отказов.
|
||||
- **Без `jobs.pid`, только эвристика `agent_runs` + потолок** — отвергнуто как
|
||||
основной механизм: не ловит зомби, чей monitor умер ДО записи `exit_code`
|
||||
(главный класс инцидента). Эвристика оставлена как Tier-2/Tier-3 поверх pid.
|
||||
- **БД-lock вместо файлового lease / внешний брокер очередей** — вне объёма
|
||||
(BRD §4), несоразмерно для single-node SQLite.
|
||||
- **Реаниматор фабрикует `exit0` и форсит `done`** — отвергнуто: ложный `done`
|
||||
без реальной работы (если git-push не случился). Выбран gate-driven advance:
|
||||
источник истины — канонический QG, не предположение об успехе.
|
||||
- **Новый статус `reaping` в enum `jobs.status`** — отвергнуто: меняет контракт
|
||||
статусов; атомарного guard `WHERE status='running'` достаточно.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Плюсы:**
|
||||
- Зомби-job самовосстанавливается БЕЗ рестарта процесса → очередь не встаёт
|
||||
(групповой риск снят для всех проектов общего инстанса).
|
||||
- Залипший lease освобождается проактивно (старт + период), не дожидаясь TTL и
|
||||
чужого acquire.
|
||||
- Незавершённый merge докатывается штатным путём, идемпотентно; ручные шаги
|
||||
оператора устранены → снят технический блокер ORCH-54.
|
||||
- Контракты неизменны (`STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, БАГ-8,
|
||||
exit-коды хука); один новый столбец через проверенный idempotent-паттерн.
|
||||
|
||||
**Минусы / ограничения:**
|
||||
- pid-liveness валиден в предположении ОДНОГО pid-namespace (агент — дочерний
|
||||
процесс оркестратора в том же контейнере). Multi-container/namespaced pid →
|
||||
pid-liveness ненадёжен; закрыто backstop'ом по времени и TTL. См. 10-tech-risks.
|
||||
- streak-счётчик in-memory best-effort: после рестарта он сбрасывается, но
|
||||
стартовый `requeue_running_jobs` покрывает рестарт-сценарий.
|
||||
- Tier-3 backstop консервативен (потолок > max timeout); очень долгий легитимный
|
||||
агент (близко к потолку) теоретически может быть реапнут — потолок выбран с
|
||||
большим запасом, чтобы этого не случалось (AC-3).
|
||||
|
||||
## Инварианты (НЕ нарушать)
|
||||
- Reaper/lease-reclaim НЕ рестартят/не роняют прод-контейнер `orchestrator` и НЕ
|
||||
инициируют git-push в `main` (AC-12). Реклейм lease — только удаление
|
||||
файла-lease, без git-операций.
|
||||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, сигнатуры/поведение `check_*` (в т.ч.
|
||||
`check_branch_mergeable`), БАГ-8 откат, exit-коды deploy-хука — без изменений;
|
||||
новых QG checks/стадий нет (AC-13).
|
||||
- never-raise на единицу фоновой работы; идемпотентность (атомарный guard +
|
||||
gate-driven advance); restart-safe; тишина при отсутствии аномалий.
|
||||
- Анти-ложноположительность (FR-1.3): живой долгий агент не реапится.
|
||||
|
||||
## Связи
|
||||
- Базируется на: ORCH-1 (очередь, adr-0002), ORCH-043 (merge-gate, adr-0006),
|
||||
ORCH-053 (reconciler-паттерн, adr-0007), ORCH-36 (self-deploy, adr-0007).
|
||||
- Сквозной ADR: adr-0011.
|
||||
- Разблокирует: ORCH-54 (полностью автономный self-deploy).
|
||||
42
docs/work-items/ORCH-065/07-infra-requirements.md
Normal file
42
docs/work-items/ORCH-065/07-infra-requirements.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# 07 — Требования к инфраструктуре (ORCH-065)
|
||||
|
||||
## Топология
|
||||
**Без изменений.** Новых контейнеров, портов, сетевых сервисов, внешних
|
||||
зависимостей нет. Job-reaper — ещё один фоновый daemon-поток ВНУТРИ существующего
|
||||
процесса оркестратора (как `queue_worker` и `reconciler`), стартует/останавливается
|
||||
в `main.lifespan`. Деплой/рестарт прод-контейнера в рамках задачи НЕ требуется и
|
||||
ЗАПРЕЩЁН (self-hosting safety) — выкатка через штатный `deploy-staging → deploy`.
|
||||
|
||||
## Допущение pid-namespace (важно для liveness-детекции)
|
||||
- Агент запускается как `subprocess.Popen(["bash","-c",cmd])` — **дочерний
|
||||
процесс оркестратора в ТОМ ЖЕ pid-namespace** (один контейнер). Значит
|
||||
`os.kill(jobs.pid, 0)` корректно отражает liveness агента, пока жив сам
|
||||
оркестратор. Это инвариант текущей упаковки (один контейнер на инстанс).
|
||||
- Lease пишет `pid = os.getpid()` — pid ПРОЦЕССА ОРКЕСТРАТОРА. После рестарта
|
||||
контейнера старый pid мёртв → детектируется. Риск переиспользования номера pid
|
||||
новым процессом закрыт условием «pid мёртв **ИЛИ** TTL истёк»: TTL добивает
|
||||
lease в любом случае (контракт ORCH-043 сохранён).
|
||||
- **Если в будущем агенты переедут в отдельные контейнеры/namespace** — Tier-1
|
||||
pid-liveness станет ненадёжной; тогда полагаемся на Tier-2 (exit_code) и Tier-3
|
||||
(потолок `reaper_max_running_s`). Зафиксировано в 10-tech-risks.
|
||||
|
||||
## Поведение при self-restart (ORCH-36 executable self-deploy)
|
||||
Self-restart прод-контейнера во время `deploy` — ровно тот сценарий, что плодит
|
||||
зомби: monitor-поток умирает вместе со старым контейнером. После рестарта:
|
||||
1. стартовый `requeue_running_jobs()` + стартовый `reclaim_stale_lease` чистят
|
||||
состояние, оставшееся от убитого процесса;
|
||||
2. периодический reaper добивает то, что возникнет позже без рестарта.
|
||||
Reaper/lease-reclaim сами НИКОГДА не рестартят и не роняют прод-контейнер и не
|
||||
делают git-push в `main` (AC-12).
|
||||
|
||||
## Эксплуатационные ручки (env, хост `.env`/`.env.staging`)
|
||||
`ORCH_REAPER_ENABLED`, `ORCH_REAPER_INTERVAL_S`, `ORCH_REAPER_DEAD_TICKS`,
|
||||
`ORCH_REAPER_MAX_RUNNING_S`, `ORCH_LEASE_RECLAIM_ENABLED`; переиспользуются
|
||||
`ORCH_MERGE_LOCK_TIMEOUT_S`, `ORCH_MERGE_GATE_REPOS`. Все флаги документируются в
|
||||
`.env.example` (developer-стадия). Полное отключение (`false`) → строго прежнее
|
||||
поведение.
|
||||
|
||||
## Документация эксплуатации
|
||||
`docs/operations/INFRA.md` — добавить (best-effort, developer/PR) короткое
|
||||
упоминание поведения reaper/lease-reclaim при self-restart. Топологическая карта
|
||||
INFRA.md не меняется.
|
||||
29
docs/work-items/ORCH-065/08-data-requirements.md
Normal file
29
docs/work-items/ORCH-065/08-data-requirements.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 08 — Требования к данным (ORCH-065)
|
||||
|
||||
## Изменение схемы: `jobs.pid`
|
||||
|
||||
| Поле | Значение |
|
||||
|------|----------|
|
||||
| Таблица | `jobs` |
|
||||
| Колонка | `pid` |
|
||||
| Тип | `INTEGER` (nullable, без DEFAULT) |
|
||||
| Назначение | pid агентского процесса (`subprocess.Popen.pid` из `launcher._spawn`) для liveness-детекции зомби job-reaper'ом (Tier-1) |
|
||||
| Механизм миграции | `_ensure_column(conn, "jobs", "pid", "INTEGER")` в `db.init_db` — идемпотентно, no-op если колонка уже есть |
|
||||
| Безопасность на live prod DB | ДА. Тот же паттерn уже применён к `jobs.transient_attempts`, `jobs.available_at`, `events.delivery_id`, `agent_runs.*`. `ALTER TABLE ADD COLUMN` в SQLite — мгновенная метаданная-операция, не блокирует и не переписывает строки |
|
||||
| Заполнение | в `_spawn` рядом с существующим `UPDATE jobs SET run_id=?, started_at=datetime('now') WHERE id=?` добавить `pid=?` (`proc.pid`). Старые строки остаются `pid IS NULL` → для них Tier-1 неприменим, работают Tier-2/Tier-3 |
|
||||
|
||||
## Что НЕ меняется
|
||||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS` — без изменений (это контракты).
|
||||
- Схема `agent_runs` — без изменений (`finished_at`/`exit_code` уже есть — основа Tier-2).
|
||||
- Файл-схема merge-lease `.merge-lease-<repo>.json` — без изменений (`pid`,
|
||||
`acquired_at`, `branch`, `work_item_id`, `task_id` уже пишутся
|
||||
`acquire_merge_lease`).
|
||||
- `jobs.status` enum (`queued|running|done|failed`) — без изменений; новый статус
|
||||
`reaping` НЕ вводится (атомарного guard `WHERE status='running'` достаточно).
|
||||
|
||||
## Совместимость / откат
|
||||
- Откат миграции не требуется: лишняя nullable-колонка безвредна при
|
||||
`reaper_enabled=false`.
|
||||
- `pid IS NULL` (строки до миграции, или если запись pid не успела) → reaper не
|
||||
делает Tier-1, опирается на Tier-2 (exit_code) и Tier-3 (потолок). Поведение
|
||||
деградирует gracefully, ложноположительных реапов не возникает.
|
||||
22
docs/work-items/ORCH-065/10-tech-risks.md
Normal file
22
docs/work-items/ORCH-065/10-tech-risks.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 10 — Технические риски (ORCH-065)
|
||||
|
||||
| # | Риск | Вероятн. | Влияние | Митигация |
|
||||
|---|------|----------|---------|-----------|
|
||||
| R-1 | **Ложноположительный реап живого долгого агента** (AC-3). Reaper помечает зомби работающий агент → потеря работы, дубль-запуск. | Сред. | Высокое | Tier-1 требует `reaper_dead_ticks`(≥2) подряд тиков мёртвого pid; живой pid = `os.kill(pid,0)` без `ProcessLookupError`. Tier-3 потолок `reaper_max_running_s` выбирается заведомо > max `agent_timeout`+grace. Юнит-тест TC-02/TC-03. |
|
||||
| R-2 | **Ложный `done` без выполненной работы.** Reaper при exit0-зомби помечает job done, хотя git-push/advance не случились (monitor умер до них). | Сред. | Высокое | Реап exit0 НЕ форсит done напрямую — идёт через **gate-driven** `_try_advance_stage`: канонический QG проверяет наличие артефакта/PR; нет артефакта → красный гейт → НЕ advance → ветка «исход неуспешен» (requeue). Источник истины — гейт, не «exit0». |
|
||||
| R-3 | **pid-reuse / namespace.** Номер pid переиспользован новым процессом → ложное «жив» (lease не реклеймится; зомби-job не реапится по Tier-1). | Низк. | Сред. | Lease: условие «pid мёртв **ИЛИ** TTL истёк» — TTL добивает в любом случае. Job-reaper: Tier-3 backstop по времени ловит то, что Tier-1 пропустил. Допущение «один pid-namespace» зафиксировано в 07-infra. |
|
||||
| R-4 | **Гонка reaper vs поздно доехавший monitor / стартовый `requeue_running_jobs`** → двойная обработка строки. | Сред. | Сред. | Атомарный reap-claim `UPDATE ... WHERE id=? AND status='running'` + проверка `rowcount` (образец `claim_next_job`). Reaper стартует ПОСЛЕ `requeue_running_jobs` в lifespan. Юнит-тест TC-06. |
|
||||
| R-5 | **Реклейм живого lease** → параллельный конфликтный merge, риск красного `main` self-hosting. | Низк. | Высокое | `reclaim_stale_lease` освобождает ТОЛЬКО при «держатель мёртв ИЛИ TTL истёк»; живой держатель в пределах TTL не трогается. holder-aware `release_merge_lease(repo, branch)`. Юнит-тест TC-12. |
|
||||
| R-6 | **Реклейм инициирует git-операцию / трогает прод-контейнер** (нарушение self-hosting safety, AC-12). | Низк. | Высокое | Реклейм = только удаление файла-lease (`os.remove`), без git. Reaper не вызывает деплой-хук/рестарт. Явный инвариант в ADR + тест/ревью. |
|
||||
| R-7 | **Идемпотентность merge не достигнута**: повторный проход стадии делает второй merge уже слитого PR. | Сред. | Сред. | never-raise guard `pr_already_merged(repo,branch)` (читает состояние PR) консультируется перед merge → уже слит = no-op. `branch_is_behind_main==False` пропускает rebase+re-test. Юнит-тест TC-16, интеграция TC-17. |
|
||||
| R-8 | **streak-счётчик in-memory теряется при рестарте** → задержка реапа или сброс прогресса. | Низк. | Низкое | Рестарт-сценарий покрыт стартовым `requeue_running_jobs` (мгновенно чистит running). Периодический reaper нужен лишь для зомби БЕЗ рестарта; сброс счётчика лишь переоткладывает реап на `reaper_dead_ticks` тиков. |
|
||||
| R-9 | **never-raise нарушен** — необработанное исключение валит daemon-поток reaper → защита тихо отключается. | Низк. | Сред. | Per-job изоляция `try/except` (образец `reconciler.reconcile_gate_once`) + внешний `try/except` в `_run`. Юнит-тест TC-08/TC-14. |
|
||||
| R-10 | **Регресс существующих тестов** merge_gate/queue/reconciler/deploy. | Низк. | Сред. | Контракты неизменны (`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/exit-коды хука); только новая колонка + новый поток + флаги (дефолт сохраняет поведение). Полный прогон `pytest tests/ -q` (regression в 04-test-plan). |
|
||||
|
||||
## Открытые вопросы / follow-up
|
||||
- **Полная автоматизация merge-финализации.** Если деплой-merge (deployer/ORCH-36
|
||||
detached host-process) окажется не полностью идемпотентным к повторному проходу,
|
||||
может понадобиться доп. работа поверх `pr_already_merged`. Здесь закрываем
|
||||
технический блокер; полный авто-approve деплоя — ORCH-54.
|
||||
- Допущение «агенты — дочерние процессы в одном pid-namespace» (R-3) должно быть
|
||||
пересмотрено, если упаковка агентов изменится (отдельные контейнеры).
|
||||
70
docs/work-items/ORCH-065/12-review.md
Normal file
70
docs/work-items/ORCH-065/12-review.md
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-065
|
||||
verdict: APPROVED
|
||||
version: 3
|
||||
---
|
||||
|
||||
# Review ORCH-065
|
||||
|
||||
## Summary
|
||||
|
||||
Задача закрывает три связанных класса отказов «процесс/поток умер, а ресурс остался
|
||||
захваченным навсегда»: zombie jobs (A), залипший merge-lease (B), неидемпотентная
|
||||
финализация merge (C). Реализация качественная: новый daemon-поток `src/job_reaper.py`
|
||||
по образцу `reconciler` (never-raise, kill-switch, снимок в `/queue`), трёхуровневая
|
||||
liveness, атомарный `reap_running_job(... WHERE status='running')`, проактивный реклейм
|
||||
lease (`pid_alive` + `reclaim_stale_lease`), идемпотентный guard `pr_already_merged`,
|
||||
колонка `jobs.pid` через идемпотентный `_ensure_column`.
|
||||
|
||||
**Все блокеры предыдущих ревью устранены:**
|
||||
- v1 P0 (guard `pr_already_merged` не подключён к merge-пути) — устранён `aa46e5d`:
|
||||
промпт `.openclaw/agents/deployer.md` консультирует `pr_already_merged` ПЕРЕД любым
|
||||
(повторным) merge (AC-11 wiring на месте, подтверждено строками 94–105/152).
|
||||
- v2 P1 (Tier-2 реапит живой финализирующий monitor; side-effects ДО атомарного claim,
|
||||
нарушение ADR-001 Р-1) — устранён `3e2eb27` двумя мерами:
|
||||
1. **Tier-2 finalization grace** — новая колонка `finished_age_s` в `get_running_jobs`
|
||||
(`src/db.py:609`) + настройка `reaper_finalize_grace_s` (дефолт 300с); Tier-2
|
||||
реапит только при `finished_age >= grace`, иначе строка не трогается
|
||||
(`src/job_reaper.py:197-209`). Живой финализирующий monitor больше не реапится
|
||||
(FR-1.3/AC-3).
|
||||
2. **claim-before-act** — `_reap_exit0` (`src/job_reaper.py:242-286`) сначала оценивает
|
||||
канонический QG read-only (`_gate_is_green` → `_run_qg`, без побочных эффектов),
|
||||
затем атомарно claim `done` ПЕРВЫМ, и только победитель claim выполняет
|
||||
`_gate_driven_advance`. Проигравший гонку (поздний monitor / стартовый requeue) не
|
||||
делает НИКАКИХ побочных эффектов → нет дубль-advance/дубль-enqueue (FR-1.2/AC-4).
|
||||
- v2 P3 (битая ссылка на adr-0011 в CHANGELOG) — исправлена в `3e2eb27`
|
||||
(`adr-0011-job-reaper-lease-reclaim.md`).
|
||||
|
||||
Инварианты сохранены (AC-13): ORCH-065-коммиты (`1a2e881`/`aa46e5d`/`3e2eb27`) НЕ касаются
|
||||
`src/stages.py` и `src/qg/checks.py` — `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/БАГ-8/
|
||||
exit-коды хука не тронуты; реклейм lease — только удаление файла, без git-операций
|
||||
(AC-12). Документация (README, internals, ADR-001, глобальный adr-0011, CHANGELOG,
|
||||
.env.example) обновлена в этом же PR (AC-17). Новые тесты покрывают grace-окно,
|
||||
lost-claim-no-side-effects, already-advanced-идемпотентность. `pytest tests/ -q` —
|
||||
**747 passed**.
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- нет
|
||||
|
||||
### P1 — Must fix
|
||||
- нет
|
||||
|
||||
### P2 — Should fix
|
||||
- нет
|
||||
|
||||
### P3 — Nice to have
|
||||
- нет
|
||||
|
||||
## Документация
|
||||
|
||||
Обновлена корректно и в этом же PR (AC-17 PASS): `docs/architecture/README.md`
|
||||
(раздел про job-reaper + lease-reclaim, таблицы БД и `/queue`),
|
||||
`docs/architecture/internals.md`, `docs/architecture/adr/adr-0011-job-reaper-lease-reclaim.md`
|
||||
(+ запись в `adr/README.md`),
|
||||
`docs/work-items/ORCH-065/06-adr/ADR-001-job-reaper-and-lease-reclaim.md`, `CHANGELOG.md`
|
||||
(ссылка на adr-0011 исправлена), `.env.example` (флаги `ORCH_REAPER_*` /
|
||||
`ORCH_REAPER_FINALIZE_GRACE_S` / `ORCH_LEASE_RECLAIM_ENABLED`). ADR-001 Р-1 и реализация
|
||||
exit0-пути теперь согласованы (claim-before-act).
|
||||
92
docs/work-items/ORCH-065/13-test-report.md
Normal file
92
docs/work-items/ORCH-065/13-test-report.md
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-065
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-065
|
||||
|
||||
Тема: job-reaper + проактивный реклейм stale/dead merge-lease + идемпотентная
|
||||
финализация merge. Прогон полного регресса в ветке
|
||||
`feature/ORCH-065-bug-zombie-jobs-merge-lease-ru`. Review-вердикт — APPROVED (v3).
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3
|
||||
- Ветка: feature/ORCH-065-bug-zombie-jobs-merge-lease-ru (worktree)
|
||||
- Прод (8500): health `200 {"status":"ok"}` — НЕ перезапускался (self-hosting инвариант соблюдён)
|
||||
- Дата: 2026-06-07
|
||||
|
||||
## Smoke API (прод 8500, read-only)
|
||||
| Endpoint | Результат |
|
||||
|----------|-----------|
|
||||
| `GET /health` | 200 `{"status":"ok","service":"orchestrator"}` |
|
||||
| `GET /status` | 200, активные задачи отдаются (ORCH-065 в `testing`, ET-013 в `development`) |
|
||||
| `GET /queue` | 200, counts/resilience/reconcile/post_deploy присутствуют |
|
||||
|
||||
Примечание: блок `reaper` в `/queue` прода (8500) ОТСУТСТВУЕТ — ожидаемо, т.к. прод
|
||||
исполняет ещё не задеплоенный (до-ORCH-065) код. Контракт блока `reaper` проверен
|
||||
тестом TC-18 (`tests/test_queue.py::test_tc18_queue_endpoint_has_reaper_block`)
|
||||
против кода ветки — PASS. Curl недоступен в окружении, smoke выполнен через
|
||||
`urllib.request` (read-only, без побочных эффектов на прод).
|
||||
|
||||
## Результаты по тест-плану (04-test-plan.yaml)
|
||||
|
||||
| TC ID | Тип | Модуль | Покрывает | Результат |
|
||||
|-------|-----|--------|-----------|-----------|
|
||||
| TC-01 | unit | test_job_reaper.py | AC-1 (реап мёртвого job без рестарта) | PASS |
|
||||
| TC-02 | unit | test_job_reaper.py | AC-3 (живой агент не реапится) | PASS |
|
||||
| TC-03 | unit | test_job_reaper.py | FR-1.3 (устойчивость reaper_dead_ticks) | PASS |
|
||||
| TC-04 | unit | test_job_reaper.py | FR-1.1/AC-1 (backstop reaper_max_running_s) | PASS |
|
||||
| TC-05 | unit | test_job_reaper.py | AC-4 (исход по результату: done/queued/failed) | PASS |
|
||||
| TC-06 | unit | test_job_reaper.py | AC-5 (атомарность reap-UPDATE guard) | PASS |
|
||||
| TC-07 | unit | test_job_reaper.py | AC-14 (kill-switch reaper_enabled=false) | PASS |
|
||||
| TC-08 | unit | test_job_reaper.py | AC-9 (never-raise per-job) | PASS |
|
||||
| TC-09 | integration | test_queue.py | AC-2 (разблокировка очереди concurrency=1) | PASS |
|
||||
| TC-10 | unit | test_merge_lease_reclaim.py | AC-6 (реклейм lease мёртвого pid) | PASS |
|
||||
| TC-11 | unit | test_merge_lease_reclaim.py | AC-7 (реклейм по TTL сохранён) | PASS |
|
||||
| TC-12 | unit | test_merge_lease_reclaim.py | AC-8 (живой lease не трогается) | PASS |
|
||||
| TC-13 | unit | test_merge_lease_reclaim.py | AC-9 (условность self-hosting/no-op) | PASS |
|
||||
| TC-14 | unit | test_merge_lease_reclaim.py | AC-9 (never-raise при ошибке lease-файла) | PASS |
|
||||
| TC-15 | unit | test_merge_lease_reclaim.py | AC-14 (kill-switch lease_reclaim_enabled=false) | PASS |
|
||||
| TC-16 | unit | test_merge_gate.py | AC-11 (идемпотентность при уже слитом PR) | PASS |
|
||||
| TC-17 | integration | test_merge_gate_race.py | AC-10 (докатывание незавершённого merge) | PASS |
|
||||
| TC-18 | integration | test_queue.py | AC-15 (блок reaper в /queue) | PASS |
|
||||
| TC-19 | unit | test_config.py | AC-13 (контракты STAGE_TRANSITIONS/QG_CHECKS неизменны) | PASS |
|
||||
| TC-20 | unit | test_config.py | §5/AC-14 (новые настройки reaper_*/lease_reclaim_*) | PASS |
|
||||
| TC-21 | unit | test_job_reaper.py | FR-2.1/AC-6 (стартовый реклейм в lifespan) | PASS |
|
||||
|
||||
Все 21 TC из плана — PASS.
|
||||
|
||||
## Сопоставление с критериями приёмки (03-acceptance-criteria.md)
|
||||
- A (AC-1…AC-5): job-reaper — покрыты TC-01..TC-06, TC-09 → PASS
|
||||
- B (AC-6…AC-9): lease-reclaim — покрыты TC-10..TC-15 → PASS
|
||||
- C (AC-10, AC-11): идемпотентная финализация — TC-16, TC-17 → PASS
|
||||
- D (AC-12 прод не трогается, AC-13 контракты, AC-14 kill-switches): TC-07, TC-15, TC-19, TC-20 + smoke прода без рестарта → PASS
|
||||
- E (AC-15 /queue, AC-16 логи/алерты): TC-18 → PASS
|
||||
- F (AC-17 документация): review подтвердил обновление README/internals/ADR-001/adr-0011/CHANGELOG/.env.example (APPROVED) → PASS
|
||||
- G (AC-18 регресс зелёный): `pytest tests/` 747 passed → PASS
|
||||
|
||||
## Вывод pytest
|
||||
|
||||
### Целевые модули плана
|
||||
```
|
||||
$ python -m pytest tests/test_job_reaper.py tests/test_merge_lease_reclaim.py \
|
||||
tests/test_merge_gate.py tests/test_merge_gate_race.py \
|
||||
tests/test_queue.py tests/test_config.py -q
|
||||
92 passed, 1 warning in 3.40s
|
||||
```
|
||||
|
||||
### Полный регресс
|
||||
```
|
||||
$ python -m pytest tests/ -v --tb=short
|
||||
======================= 747 passed, 1 warning in 15.47s ========================
|
||||
```
|
||||
(1 warning — PydanticDeprecatedSince20 в src/config.py, не связан с ORCH-065,
|
||||
предсуществующий.)
|
||||
|
||||
## Итог
|
||||
**PASS.** Полный регресс — 747 passed, 0 failed. Все 21 TC тест-плана зелёные,
|
||||
все критерии приёмки (AC-1…AC-18) подтверждены. Smoke прода — health/status/queue
|
||||
200 OK, прод-контейнер не перезапускался (self-hosting инвариант соблюдён).
|
||||
Задача готова к переходу на стадию `deploy-staging`.
|
||||
32
docs/work-items/ORCH-065/15-staging-log.md
Normal file
32
docs/work-items/ORCH-065/15-staging-log.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-07T16:13:48Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed against the live staging environment (8501),
|
||||
run canonically inside the `orchestrator-staging` container (ORCH-048, ADR-001).
|
||||
|
||||
**Verdict:** SUCCESS — `staging_check.py` exited **0**. All REAL (pipeline)
|
||||
checks green; the only failures are the two known sandbox-infra checks
|
||||
(C9a/C9b), which are waived per ORCH-061 because every REAL check passed.
|
||||
|
||||
## Result
|
||||
|
||||
- RESULT: 8/10 checks PASS
|
||||
- REAL failed: none
|
||||
- SANDBOX_INFRA failed (waived): C9a Branch appears in orchestrator-sandbox; C9b Analyst job enqueued in staging queue
|
||||
|
||||
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
|
||||
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
|
||||
|
||||
## Block detail
|
||||
|
||||
- [Block A] SMOKE — A1 /health, A2 /queue, A3 ORCH_STAGING=true → PASS
|
||||
- [Block B] ACCESS — B4 Plane sandbox, B5 Gitea orchestrator-sandbox (push=true), B6 registry isolation (sandbox present, prod ET/ORCH absent) → PASS
|
||||
- [Block C] E2E (stub) — C7 create issue in SANDBOX, C8 trigger pipeline via /webhook/plane → PASS; C9a/C9b → FAIL (sandbox-infra, waived)
|
||||
- CLEANUP — Plane issue deleted (HTTP 204)
|
||||
|
||||
tolerance: staging_infra_tolerance_enabled=True
|
||||
7
docs/work-items/ORCH-066/00-business-request.md
Normal file
7
docs/work-items/ORCH-066/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: [высокий] Статусная модель Plane: осмысленные статусы этапов
|
||||
|
||||
Work Item ID: ORCH-066
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
110
docs/work-items/ORCH-066/01-brd.md
Normal file
110
docs/work-items/ORCH-066/01-brd.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# 01 — Business Requirements Document (BRD)
|
||||
|
||||
**Work Item:** ORCH-066
|
||||
**Заголовок:** [высокий] Статусная модель Plane: осмысленные статусы этапов
|
||||
**Стадия:** analysis
|
||||
**Автор:** Analyst
|
||||
**Дата:** 2026-06-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Контекст и проблема
|
||||
|
||||
Статусная модель Plane оркестратора имеет **семантические перегрузки**: один и тот
|
||||
же Plane-статус используется для несовместимых смыслов, из-за чего:
|
||||
|
||||
- оператор не понимает, на каком реально этапе стоит задача (доска нечитаема);
|
||||
- повышается риск ошибки оператора (например, неверный ручной перевод статуса);
|
||||
- `In Progress` одновременно означает «человек запускает конвейер», «идёт анализ»,
|
||||
«идёт прод-деплой» и «возврат из Needs Input» — четыре разных смысла на одном статусе.
|
||||
|
||||
Уже частично исправлено: ORCH-059 ввёл отдельный статус для подтверждения деплоя
|
||||
(`Confirm Deploy`), разгрузив перегруженный `Approved`. ORCH-066 завершает наведение
|
||||
порядка по **утверждённой Owner** статусной модели.
|
||||
|
||||
### Два слоя (критично различать)
|
||||
|
||||
| Слой | Что это | Источник | Трогаем? |
|
||||
|------|---------|----------|----------|
|
||||
| **A** | `STAGE_TRANSITIONS` — внутренняя машина стадий (`created→analysis→…→done`) | `src/stages.py` | **НЕТ (инвариант)** |
|
||||
| **B** | Plane-статусы — индикация на доске | `src/plane_sync.py` + точки в `src/stage_engine.py` / `src/webhooks/plane.py` | **ДА** |
|
||||
|
||||
ORCH-066 меняет **только слой B** и точки, где код вручную проставляет Plane-статусы.
|
||||
|
||||
---
|
||||
|
||||
## 2. Целевая статусная модель (решение Owner)
|
||||
|
||||
```
|
||||
Backlog → Todo → [To Analyse] → Analysis → [In Review → Approved] → Architecture →
|
||||
Development → Code-Review → Testing → Awaiting Deploy → [Confirm Deploy] → Deploying →
|
||||
Monitoring after Deploy → Done
|
||||
```
|
||||
|
||||
- `[...]` = **действие человека** (вход-триггер).
|
||||
- Остальное ставит **орк** (индикация).
|
||||
|
||||
### Ветки (нелинейные исходы)
|
||||
- **Rejected** — откат на предыдущую стадию (человек).
|
||||
- **Needs Input** — ТОЛЬКО аналитик (НЕ расширять на других агентов).
|
||||
- **Blocked** — затык / фейл деплоя / деградация прода.
|
||||
- **Cancelled** — человек решил не делать задачу (валидный выход из In Review).
|
||||
|
||||
---
|
||||
|
||||
## 3. Бизнес-требования
|
||||
|
||||
| ID | Требование | Приоритет |
|
||||
|----|------------|-----------|
|
||||
| **BR-1** | Каждый этап конвейера показывается на доске Plane осмысленным статусом (To Analyse / Analysis / Code-Review / Awaiting Deploy / Deploying / Monitoring after Deploy). | Must |
|
||||
| **BR-2** | `To Analyse` — единый человеческий вход: (а) старт нового конвейера, (б) resume/relaunch аналитика при возврате из Needs Input. Заменяет роль `In Progress` как входа-триггера. | Must |
|
||||
| **BR-3** | Стадия `analysis` индицируется отдельным статусом `Analysis` (орк ставит при старте/relaunch аналитика), а не `In Progress`. | Must |
|
||||
| **BR-4** | Стадия `review` индицируется Plane-статусом `Code-Review` (переименование `Review`). | Must |
|
||||
| **BR-5** | Self-deploy Phase A (approval-pending) ставит `Awaiting Deploy` вместо `In Review`. | Must |
|
||||
| **BR-6** | Self-deploy Phase B (старт прод-деплоя) ставит `Deploying`. | Must |
|
||||
| **BR-7** | Self-deploy Phase C (health-OK финализация) ставит `Monitoring after Deploy` (НЕ `Done` сразу). | Must |
|
||||
| **BR-8** | Post-deploy monitor (ORCH-021): чистое закрытие окна (HEALTHY) → `Done`; UNHEALTHY/деградация → `Blocked`. | Must |
|
||||
| **BR-9** | `In Review` разгрузить: оставить ТОЛЬКО за approve-pending артефактов конвейера (BRD/ревью). Выходы: `Approved` (вперёд), `Rejected` (откат), `Cancelled` (человек отменил). | Must |
|
||||
| **BR-10** | `Needs Input` — БЕЗ ИЗМЕНЕНИЙ. Остаётся только у аналитика (`01-questions.md` → `set_issue_needs_input`). Механизм не трогать. | Must |
|
||||
| **BR-11** | Возврат аналитика из Needs Input выполняется через `To Analyse` (а НЕ через `In Progress`). Логика fork «старт vs resume» (по наличию task + active-job) сохраняется. | Must (грабли R1) |
|
||||
| **BR-12** | **Fail-closed:** отсутствие нового статуса в проекте (enduro / Plane API down / fallback `_DEFAULT_STATES`) НЕ приводит к падению; поведение остаётся backward-compatible (паттерн ORCH-059 AC-7). | Must |
|
||||
| **BR-13** | Reconciler не «оживляет» активные ожидания (`Awaiting Deploy` / `Deploying` / `Monitoring after Deploy`) как зависшие задачи (Guard 2 skip-list). | Must |
|
||||
| **BR-14** | Документация (golden source) обновлена в том же PR: `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md`, ADR per-work-item. | Must |
|
||||
|
||||
---
|
||||
|
||||
## 4. Границы (Out of Scope / НЕ трогать)
|
||||
|
||||
- `STAGE_TRANSITIONS` (`src/stages.py`) — машина стадий, инвариант.
|
||||
- `QG_CHECKS`, `check_deploy_status`, exit-коды хука (0/1/2), merge-gate, схема БД.
|
||||
- `Confirm Deploy` (уже работает, ORCH-059).
|
||||
- Механизм `Needs Input` (analyst-only) — не расширять, не менять.
|
||||
- Поведение прод-деплоя **не-self** репозиториев (enduro-trails): для них терминальный
|
||||
переход остаётся `deploy → Done` как сейчас (Monitoring after Deploy не применяется —
|
||||
post-deploy monitor армится только для self-hosting).
|
||||
- Автоматический approve / авто-rollback self-hosting (ORCH-54 / ORCH-021 политика
|
||||
ALERT_ONLY) — не меняется.
|
||||
|
||||
---
|
||||
|
||||
## 5. Инфра-предусловие (вне кода, делает оператор)
|
||||
|
||||
Новые Plane-статусы в проекте **ORCH** создаёт оператор через Plane API **ДО** эксплуатации:
|
||||
`To Analyse`, `Analysis`, `Code-Review`, `Awaiting Deploy`, `Deploying`,
|
||||
`Monitoring after Deploy` (`Confirm Deploy` уже есть).
|
||||
|
||||
Резолвер (`_PLANE_NAME_TO_KEY` + `get_project_states`) подхватывает их **по имени** с
|
||||
**fail-closed fallback** на `_DEFAULT_STATES` (см. BR-12). Документируется в
|
||||
`07-infra-requirements.md` (создаёт архитектор) и в `docs/operations/`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Definition of Done
|
||||
|
||||
- Plane показывает осмысленные статусы на каждом этапе.
|
||||
- Возврат аналитика из Needs Input работает через `To Analyse`.
|
||||
- Phase A → `Awaiting Deploy`, Phase B → `Deploying`, Phase C → `Monitoring after Deploy`,
|
||||
окно HEALTHY → `Done`, фейл → `Blocked`.
|
||||
- `STAGE_TRANSITIONS` не изменён.
|
||||
- `pytest tests/ -q` — зелёный. Fail-closed покрыт тестами.
|
||||
- Документация обновлена.
|
||||
178
docs/work-items/ORCH-066/02-trz.md
Normal file
178
docs/work-items/ORCH-066/02-trz.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# 02 — Техническое задание (ТЗ)
|
||||
|
||||
**Work Item:** ORCH-066
|
||||
**Стадия:** analysis → (вход для architecture)
|
||||
**Автор:** Analyst
|
||||
|
||||
> ТЗ фиксирует ТРЕБУЕМОЕ ПОВЕДЕНИЕ и затронутые точки кода. Конкретную архитектуру
|
||||
> резолвера (точные имена ключей/функций) финализирует архитектор в ADR. Ниже —
|
||||
> опорный контракт, согласованный с бизнес-запросом Owner.
|
||||
|
||||
---
|
||||
|
||||
## 1. Задействованные модули `src/`
|
||||
|
||||
| Модуль | Роль в задаче |
|
||||
|--------|---------------|
|
||||
| `src/plane_sync.py` | **Ядро изменений (слой B):** реестр логических статусов (`_DEFAULT_STATES`), `_PLANE_NAME_TO_KEY`, маппинг стадия→статус (`_STAGE_TO_STATE_KEY`, `STAGE_VISIBILITY_STATE`), хелперы `set_issue_*`. |
|
||||
| `src/webhooks/plane.py` | Маршрутизация входящего статуса (`handle_issue_updated`): `To Analyse` → `handle_status_start` (старт **или** resume). |
|
||||
| `src/stage_engine.py` | Точки ручной простановки статуса: analyst-flow (`Analysis`/`Needs Input`/`In Review`), Phase A (`Awaiting Deploy`), Phase B (`Deploying`), Phase C → `Monitoring after Deploy`, post-deploy monitor → `Done`/`Blocked`. |
|
||||
| `src/reconciler.py` | F-2 запрос статусов (`To Analyse` в список), Guard 2 skip-list (активные ожидания). |
|
||||
| `src/stages.py` | **НЕ менять** (инвариант слоя A). Используется только для чтения переходов. |
|
||||
| `src/config.py` | (При необходимости) kill-switch для новой статусной модели — на усмотрение архитектора (см. §6). |
|
||||
|
||||
---
|
||||
|
||||
## 2. Изменения статусной модели (слой B)
|
||||
|
||||
### 2.1. Реестр логических статусов (`src/plane_sync.py`)
|
||||
|
||||
Ввести новые **логические ключи** и их имена в `_PLANE_NAME_TO_KEY`:
|
||||
|
||||
| Логический ключ | Plane name | Назначение |
|
||||
|-----------------|-----------|------------|
|
||||
| `to_analyse` | `To Analyse` | Вход-триггер (старт + resume аналитика). |
|
||||
| `analysis` | `Analysis` | Индикация стадии analysis (орк). |
|
||||
| `code_review` | `Code-Review` | Индикация стадии review (орк). Заменяет `review`. |
|
||||
| `awaiting_deploy` | `Awaiting Deploy` | Phase A approval-pending (орк). |
|
||||
| `deploying` | `Deploying` | Phase B прод-деплой идёт (орк). |
|
||||
| `monitoring` | `Monitoring after Deploy` | Phase C / post-deploy окно (орк). |
|
||||
|
||||
Сохранить существующие: `backlog`, `todo`, `in_progress` (backward-compat), `needs_input`,
|
||||
`in_review`, `blocked`, `done`, `cancelled`, `architecture`, `development`, `testing`,
|
||||
`approved`, `rejected`. `Cancelled` уже присутствует в `_PLANE_NAME_TO_KEY`.
|
||||
|
||||
### 2.2. Fail-closed резолюция (КРИТИЧНО — BR-12)
|
||||
|
||||
`get_project_states()` после резолва по API делает `setdefault(k, v)` из `_DEFAULT_STATES`.
|
||||
Чтобы отсутствие нового статуса в проекте (enduro / Plane down / частичная конфигурация)
|
||||
**не ломало** конвейер, новые логические ключи в `_DEFAULT_STATES` должны
|
||||
**алиаситься на существующие UUID** (degrade-to-current):
|
||||
|
||||
| Новый ключ | Default-алиас (UUID) | Деградированное поведение |
|
||||
|------------|----------------------|---------------------------|
|
||||
| `to_analyse` | = `in_progress` | enduro/старый проект: `In Progress` по-прежнему триггерит старт/resume. |
|
||||
| `analysis` | = `in_progress` | analysis показывается как `In Progress` (как сейчас). |
|
||||
| `code_review` | = `review` | review показывается как `Review` (как сейчас). |
|
||||
| `awaiting_deploy` | = `in_review` | Phase A показывается как `In Review` (как сейчас). |
|
||||
| `deploying` | = `in_progress` | Phase B показывается как `In Progress` (как сейчас). |
|
||||
| `monitoring` | = `done` | Phase C показывается как `Done` (как сейчас); монитор затем держит Done / флипает Blocked. |
|
||||
|
||||
> Эффект: если оператор НЕ создал новый статус — система работает строго как до ORCH-066
|
||||
> (никаких падений, никаких 404 от Plane PATCH). Если создал — резолвится по имени и
|
||||
> используется новый UUID. Это ровно паттерн ORCH-059 AC-7.
|
||||
|
||||
### 2.3. Маппинг стадия → статус
|
||||
|
||||
`src/plane_sync.py`:
|
||||
- `_STAGE_TO_STATE_KEY`: `analysis` → `analysis` (было `in_progress`); `review` → `code_review`
|
||||
(было `review`). `deploy` остаётся (управляется Phase A/B/C напрямую, не через
|
||||
`update_issue_state`). `created`/`architecture`/`development`/`testing`/`done` — без изменений.
|
||||
- `STAGE_VISIBILITY_STATE`: `review` → `code_review` (было `review`). Добавить
|
||||
`analysis` → `analysis`, если индикация analysis ставится через `set_issue_stage_state`
|
||||
(решает архитектор; альтернатива — отдельный хелпер `set_issue_analysis`).
|
||||
- Сохранить совместимость `STAGE_TO_STATE` / `PLANE_STATES` алиасов (импортируются тестами).
|
||||
|
||||
### 2.4. Точки простановки статуса
|
||||
|
||||
| Место (файл:симв.) | Сейчас | Должно стать |
|
||||
|--------------------|--------|--------------|
|
||||
| `webhooks/plane.py` `handle_issue_updated` | `new_state == in_progress` → `handle_status_start` | `new_state == to_analyse` (с fail-closed: при алиасе совпадает с `in_progress`) → `handle_status_start` |
|
||||
| `webhooks/plane.py` `start_pipeline` (старт) | статус остаётся `In Progress` | при старте/enqueue analyst орк ставит `Analysis` |
|
||||
| `webhooks/plane.py` `handle_status_start` (resume из Needs Input) | relaunch на `In Progress`-триггере | relaunch на `To Analyse`-триггере; при relaunch орк ставит `Analysis`. Fork «старт vs resume» (по `get_task_by_plane_id` + `has_active_job_for_task`) — **сохранить как есть.** |
|
||||
| `stage_engine.py` `_handle_analysis_approved_flow` (artifacts ready) | `set_issue_in_review` | оставить `In Review` (BR-9: In Review только за approve-pending конвейера) ✔ без изменений |
|
||||
| `stage_engine.py` `_handle_analysis_approved_flow` (questions) | `set_issue_needs_input` | **без изменений** (BR-10) |
|
||||
| `stage_engine.py` `_handle_self_deploy_phase_a` | `set_issue_in_review` | `Awaiting Deploy` (`set_issue_awaiting_deploy` или аналог) |
|
||||
| `stage_engine.py` `_handle_self_deploy_phase_b` | (статус не меняет) | `Deploying` |
|
||||
| `stage_engine.py` advance `deploy → done` (terminal-sync, строка ~338) | `set_issue_done` для всех | **self-hosting:** `Monitoring after Deploy` (перед/вместо арма монитора); **не-self:** `Done` как сейчас |
|
||||
| `stage_engine.py` `run_post_deploy_monitor` (HEALTHY, окно закрыто) | пишет лог + коммент, статус Plane НЕ трогает (остаётся Done) | `Done` (явно) |
|
||||
| `stage_engine.py` `run_post_deploy_monitor` (DEGRADED) | пишет лог + alert | `Blocked` |
|
||||
|
||||
> **Замечание по terminal-sync (важно для архитектора):** сейчас `advance_stage` на
|
||||
> `next_stage == "done"` вызывает `set_issue_done` безусловно (строка ~338), затем армит
|
||||
> post-deploy monitor для self-hosting (~361). Нужно развести: для репо, где
|
||||
> `post_deploy.post_deploy_applies(repo)` истинно (self-hosting) — ставить `Monitoring
|
||||
> after Deploy` вместо `Done`, и переложить простановку `Done`/`Blocked` на финал
|
||||
> монитора (`run_post_deploy_monitor`). Для прочих репо — `Done` как сейчас.
|
||||
|
||||
### 2.5. Новые хелперы `src/plane_sync.py`
|
||||
|
||||
Добавить тонкие обёртки по образцу `set_issue_in_review` (резолв per-project UUID +
|
||||
`_set_issue_state_direct`), never-raise при отсутствии issue:
|
||||
- `set_issue_analysis(work_item_id, project_id=None)`
|
||||
- `set_issue_code_review(...)` (или через `set_issue_stage_state("review")`)
|
||||
- `set_issue_awaiting_deploy(...)`
|
||||
- `set_issue_deploying(...)`
|
||||
- `set_issue_monitoring(...)`
|
||||
|
||||
(Точный набор/именование — на усмотрение архитектора; контракт: per-project резолв +
|
||||
fail-closed.)
|
||||
|
||||
---
|
||||
|
||||
## 3. Изменения reconciler (`src/reconciler.py`)
|
||||
|
||||
- **F-2** `_reconcile_plane_project`: добавить `to_analyse` в список запрашиваемых
|
||||
статусов (`list_issues_by_state([... , to_analyse])`) и в `_reconcile_plane_issue`
|
||||
маршрутизировать `new_state == to_analyse` → `handle_status_start` (старт при `task is
|
||||
None`, resume при существующем task без active-job — логика уже в `handle_status_start`).
|
||||
Сохранить обработку `approved`/`rejected`. При fail-closed алиасе `to_analyse==in_progress`
|
||||
поведение не дублируется (один и тот же UUID).
|
||||
- **Guard 2** `_is_blocked_or_needs_input` (F-1 skip): расширить skip-множество активными
|
||||
ожиданиями — `awaiting_deploy`, `deploying`, `monitoring` — чтобы реконсилер НЕ
|
||||
«оживлял» их как зависшие (BR-13). Имя метода/семантику можно обобщить
|
||||
(«human-or-active-wait»), флаг `reconcile_skip_blocked_enabled` продолжает управлять
|
||||
этим networked-чеком.
|
||||
|
||||
> Примечание: F-1 и так не тронет Phase A (`check_deploy_status` red → silent),
|
||||
> Deploying (active finalizer job), Monitoring (стадия `done`). Guard 2 — явная
|
||||
> defense-in-depth по требованию Owner.
|
||||
|
||||
---
|
||||
|
||||
## 4. Изменения API / эндпоинтов
|
||||
|
||||
**Нет** новых HTTP-эндпоинтов. `GET /queue` / `GET /status` — без изменений контракта
|
||||
(статусы Plane там не отражаются). Изменения только во внешней индикации Plane (PATCH
|
||||
issue state — существующий механизм).
|
||||
|
||||
---
|
||||
|
||||
## 5. Изменения схемы БД
|
||||
|
||||
**Нет.** `tasks` не хранит Plane-статус (источник истины — стадия в БД + Plane API).
|
||||
Миграции не требуются.
|
||||
|
||||
---
|
||||
|
||||
## 6. Требования к новым QG checks
|
||||
|
||||
**Нет.** `QG_CHECKS` не расширяется. Статусы — индикация, не управление (канон:
|
||||
машинные вердикты читаются из YAML-frontmatter артефактов, не из Plane-статуса).
|
||||
|
||||
Опционально (на усмотрение архитектора): единый kill-switch новой статусной модели
|
||||
(env-флаг) для безопасного раската, по образцу `staging_infra_tolerance_enabled` /
|
||||
`reconcile_skip_blocked_enabled`. Не обязателен, т.к. fail-closed алиасинг (§2.2) уже даёт
|
||||
backward-compatible деградацию.
|
||||
|
||||
---
|
||||
|
||||
## 7. Артефакты pipeline, создаваемые/обновляемые
|
||||
|
||||
- `06-adr/ADR-001-plane-status-model.md` — архитектор (решение по резолверу,
|
||||
алиасингу, разводке terminal-sync).
|
||||
- `07-infra-requirements.md` — архитектор (список Plane-статусов для ручного создания
|
||||
оператором + Plane API инструкция).
|
||||
- Документация (golden source, тот же PR): `CLAUDE.md` (секция статусной модели),
|
||||
`docs/architecture/README.md` (секция статусов рядом с ORCH-036/ORCH-021),
|
||||
`CHANGELOG.md`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Инварианты (проверяемые)
|
||||
|
||||
- `src/stages.py` `STAGE_TRANSITIONS` — байт-в-байт без изменений.
|
||||
- `QG_CHECKS`, `check_deploy_status`/`_parse_deploy_status`, exit-коды хука, merge-gate,
|
||||
схема БД, `Confirm Deploy`, механизм `Needs Input` — без изменений.
|
||||
- Все новые `set_issue_*` / резолв — never-raise (Plane down ⇒ degrade, не crash).
|
||||
- Поведение enduro (не-self) и его терминальный `Done` — без регресса.
|
||||
71
docs/work-items/ORCH-066/03-acceptance-criteria.md
Normal file
71
docs/work-items/ORCH-066/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# 03 — Критерии приёмки (Acceptance Criteria)
|
||||
|
||||
**Work Item:** ORCH-066
|
||||
|
||||
Каждый критерий — чёткое условие PASS/FAIL. Покрытие тестами — см. `04-test-plan.yaml`.
|
||||
|
||||
---
|
||||
|
||||
## Группа A — Вход и стадия анализа
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-1** | `To Analyse` запускает конвейер | Перевод issue без task в `To Analyse` → `handle_status_start` → `start_pipeline` (создаётся task, ветка, enqueue analyst). | Не запускается / запускается на другом статусе. |
|
||||
| **AC-2** | `To Analyse` делает resume аналитика из Needs Input | Существующий task без active-job + перевод в `To Analyse` → relaunch агента текущей стадии (analyst читает свежие комменты). Fork «старт vs resume» определяется по `get_task_by_plane_id` + `has_active_job_for_task` (как раньше). | Создаётся второй task / двойной запуск / resume не происходит. |
|
||||
| **AC-3** | Стадия `analysis` индицируется статусом `Analysis` | При старте/relaunch аналитика орк ставит `Analysis`. | Остаётся `In Progress` (при наличии статуса `Analysis` в проекте). |
|
||||
| **AC-4** | Busy-guard сохранён | `To Analyse` при существующем active-job для task → НЕ relaunch (no double launch). | Двойной запуск агента. |
|
||||
|
||||
## Группа B — Code-Review
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-5** | Стадия `review` индицируется `Code-Review` | Вход в стадию `review` → Plane-статус `Code-Review`. | Остаётся `Review` (при наличии нового статуса). |
|
||||
|
||||
## Группа C — Self-deploy фазы
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-6** | Phase A → `Awaiting Deploy` | `_handle_self_deploy_phase_a` ставит `Awaiting Deploy` (не `In Review`). | Ставит `In Review` (при наличии нового статуса). |
|
||||
| **AC-7** | Phase B → `Deploying` | `_handle_self_deploy_phase_b` при успешном `initiate_deploy` ставит `Deploying`. | Статус не меняется / иной. |
|
||||
| **AC-8** | Phase C → `Monitoring after Deploy` (self) | Финализатор SUCCESS для self-hosting → статус `Monitoring after Deploy`, НЕ `Done` сразу. | Ставит `Done` немедленно (для self-hosting). |
|
||||
| **AC-9** | Не-self deploy → `Done` без регресса | Для не-self репо (`post_deploy_applies==False`) терминальный `deploy → done` ставит `Done` как сейчас. | Не-self репо получает `Monitoring after Deploy` / иной регресс. |
|
||||
|
||||
## Группа D — Post-deploy monitor
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-10** | Чистое окно → `Done` | `run_post_deploy_monitor` HEALTHY + окно исчерпано → статус `Done`. | Остаётся `Monitoring after Deploy` / иной. |
|
||||
| **AC-11** | Деградация → `Blocked` | `run_post_deploy_monitor` DEGRADED → статус `Blocked` (+ существующий ALERT_ONLY для self). | Остаётся в Monitoring / ставит Done. |
|
||||
| **AC-12** | Self-hosting монитор не рестартит прод | Тик НИКОГДА не рестартит/откатывает прод-контейнер (ORCH-021 BR-5 сохранён). | Тик трогает прод-контейнер. |
|
||||
|
||||
## Группа E — In Review / Needs Input / ветки
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-13** | `In Review` только за approve-pending конвейера | `In Review` ставится лишь для approve артефактов (analyst BRD/ревью), не для Phase A. | Phase A / иные стадии ставят `In Review`. |
|
||||
| **AC-14** | `Needs Input` без изменений | Поведение `set_issue_needs_input` (analyst `01-questions.md`) идентично прежнему; не расширено на других агентов. | Механизм изменён / расширен. |
|
||||
| **AC-15** | `Cancelled` — валидный выход из In Review без действий конвейера | Перевод в `Cancelled` → орк не выполняет advance/rollback (индикация, не управление). | Орк совершает действие конвейера на `Cancelled`. |
|
||||
|
||||
## Группа F — Fail-closed (КРИТИЧНО)
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-16** | Отсутствие нового статуса не ломает конвейер | Проект без новых статусов (enduro/частичный/Plane down) → `get_project_states` отдаёт default-алиасы; все `set_issue_*`/триггеры работают backward-compatible, без исключений и без 404 PATCH. | Падение / необработанное исключение / зависание задачи. |
|
||||
| **AC-17** | enduro `In Progress` по-прежнему стартует конвейер | Через `to_analyse`-алиас (= `in_progress` UUID) перевод enduro-issue в `In Progress` запускает старт/resume. | enduro-старт сломан. |
|
||||
| **AC-18** | Резолв по имени | При наличии статуса в проекте по `name` (`_PLANE_NAME_TO_KEY`) используется его UUID, а не default-алиас. | Используется неверный UUID. |
|
||||
|
||||
## Группа G — Reconciler
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-19** | F-2 реконсилирует `To Analyse` | `_reconcile_plane_project` запрашивает `to_analyse` и маршрутизирует к `handle_status_start` (старт/resume при потерянном webhook). | `To Analyse`-старты не реконсилируются. |
|
||||
| **AC-20** | Guard 2 skip активных ожиданий | Задачи в `Awaiting Deploy` / `Deploying` / `Monitoring after Deploy` НЕ «оживляются» F-1 как зависшие. | Реконсилер advance'ит активное ожидание. |
|
||||
|
||||
## Группа H — Инварианты и документация
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-21** | `STAGE_TRANSITIONS` не изменён | `src/stages.py` `STAGE_TRANSITIONS` идентичен (diff пуст). | Любое изменение слоя A. |
|
||||
| **AC-22** | Реестры/контракты не изменены | `QG_CHECKS`, `check_deploy_status`, exit-коды хука, merge-gate, схема БД, `Confirm Deploy` — без изменений. | Любое изменение перечисленного. |
|
||||
| **AC-23** | Тесты зелёные | `pytest tests/ -q` проходит полностью; новые fail-closed тесты присутствуют и зелёные. | Любой красный тест. |
|
||||
| **AC-24** | Документация обновлена (golden source) | `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md` обновлены; заведён `06-adr/ADR-001-*`. | Любой из артефактов не обновлён. |
|
||||
184
docs/work-items/ORCH-066/04-test-plan.yaml
Normal file
184
docs/work-items/ORCH-066/04-test-plan.yaml
Normal file
@@ -0,0 +1,184 @@
|
||||
work_item: ORCH-066
|
||||
description: >
|
||||
Тест-план статусной модели Plane (слой B). Покрывает осмысленные статусы этапов,
|
||||
возврат аналитика через To Analyse, фазы self-deploy, post-deploy monitor,
|
||||
fail-closed деградацию и reconciler. Слой A (STAGE_TRANSITIONS) проверяется на
|
||||
неизменность. Все тесты — pytest; Plane API мокается (httpx), как в существующих
|
||||
tests/test_plane_*.py / tests/test_orch10_states.py.
|
||||
|
||||
tests:
|
||||
# --- Группа A: вход и стадия анализа ---
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "To Analyse без существующего task -> handle_status_start -> start_pipeline (старт конвейера)."
|
||||
module: tests/test_status_trigger.py
|
||||
covers: [AC-1]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: integration
|
||||
description: "To Analyse при существующем task без active-job -> relaunch агента стадии (resume из Needs Input), новый task НЕ создаётся."
|
||||
module: tests/test_plane_to_analyse_resume.py
|
||||
covers: [AC-2, BR-11]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: "Старт/relaunch аналитика ставит Plane-статус Analysis (а не In Progress) при наличии статуса в проекте."
|
||||
module: tests/test_plane_status_model.py
|
||||
covers: [AC-3]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: "To Analyse при существующем task с active-job -> НЕ relaunch (busy-guard)."
|
||||
module: tests/test_plane_to_analyse_resume.py
|
||||
covers: [AC-4]
|
||||
expected: PASS
|
||||
|
||||
# --- Группа B: Code-Review ---
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: "Вход в стадию review -> Plane-статус Code-Review (маппинг _STAGE_TO_STATE_KEY / STAGE_VISIBILITY_STATE)."
|
||||
module: tests/test_plane_status_model.py
|
||||
covers: [AC-5]
|
||||
expected: PASS
|
||||
|
||||
# --- Группа C: self-deploy фазы ---
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: "_handle_self_deploy_phase_a ставит Awaiting Deploy (не In Review)."
|
||||
module: tests/test_deploy_approve.py
|
||||
covers: [AC-6, AC-13]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: "_handle_self_deploy_phase_b при успешном initiate_deploy ставит Deploying."
|
||||
module: tests/test_deploy_approve.py
|
||||
covers: [AC-7]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: integration
|
||||
description: "Phase C (finalizer SUCCESS) для self-hosting ставит Monitoring after Deploy, НЕ Done; армит post-deploy monitor."
|
||||
module: tests/test_deploy_terminal_sync.py
|
||||
covers: [AC-8]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: integration
|
||||
description: "Не-self репо: deploy->done ставит Done (без регресса, Monitoring не применяется)."
|
||||
module: tests/test_deploy_terminal_sync.py
|
||||
covers: [AC-9]
|
||||
expected: PASS
|
||||
|
||||
# --- Группа D: post-deploy monitor ---
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: "run_post_deploy_monitor HEALTHY + окно исчерпано -> Plane-статус Done."
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-10]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: "run_post_deploy_monitor DEGRADED -> Plane-статус Blocked (+ ALERT_ONLY для self)."
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-11]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: "Self-hosting тик НЕ рестартит/не откатывает прод-контейнер (ORCH-021 BR-5 сохранён)."
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-12]
|
||||
expected: PASS
|
||||
|
||||
# --- Группа E: In Review / Needs Input / Cancelled ---
|
||||
- id: TC-13
|
||||
type: unit
|
||||
description: "In Review ставится только за approve-pending конвейера (analyst BRD ready), не Phase A."
|
||||
module: tests/test_analyst_status_only_regression.py
|
||||
covers: [AC-13]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-14
|
||||
type: unit
|
||||
description: "set_issue_needs_input (analyst 01-questions.md) поведение идентично прежнему; не расширено на других агентов."
|
||||
module: tests/test_plane_status_model.py
|
||||
covers: [AC-14, BR-10]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-15
|
||||
type: unit
|
||||
description: "Перевод в Cancelled -> handle_issue_updated не выполняет advance/rollback (индикация, не управление)."
|
||||
module: tests/test_plane_webhook.py
|
||||
covers: [AC-15]
|
||||
expected: PASS
|
||||
|
||||
# --- Группа F: fail-closed (критично) ---
|
||||
- id: TC-16
|
||||
type: unit
|
||||
description: "Проект без новых статусов: get_project_states отдаёт default-алиасы (to_analyse=in_progress, code_review=review, awaiting_deploy=in_review, monitoring=done); исключений нет."
|
||||
module: tests/test_plane_status_failclosed.py
|
||||
covers: [AC-16, BR-12]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-17
|
||||
type: unit
|
||||
description: "Plane API down -> get_project_states fallback на _DEFAULT_STATES; set_issue_* never-raise."
|
||||
module: tests/test_plane_status_failclosed.py
|
||||
covers: [AC-16]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-18
|
||||
type: integration
|
||||
description: "enduro In Progress по-прежнему стартует конвейер через to_analyse-алиас."
|
||||
module: tests/test_plane_status_failclosed.py
|
||||
covers: [AC-17]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-19
|
||||
type: unit
|
||||
description: "Резолв по имени: при наличии статуса в проекте используется его UUID, а не default-алиас."
|
||||
module: tests/test_orch10_states.py
|
||||
covers: [AC-18]
|
||||
expected: PASS
|
||||
|
||||
# --- Группа G: reconciler ---
|
||||
- id: TC-20
|
||||
type: integration
|
||||
description: "F-2 _reconcile_plane_project запрашивает to_analyse и маршрутизирует к handle_status_start (потерянный webhook старта/resume)."
|
||||
module: tests/test_reconciler_plane.py
|
||||
covers: [AC-19]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-21
|
||||
type: unit
|
||||
description: "Guard 2: задачи в Awaiting Deploy / Deploying / Monitoring after Deploy НЕ оживляются F-1 как зависшие."
|
||||
module: tests/test_reconciler.py
|
||||
covers: [AC-20, BR-13]
|
||||
expected: PASS
|
||||
|
||||
# --- Группа H: инварианты ---
|
||||
- id: TC-22
|
||||
type: unit
|
||||
description: "STAGE_TRANSITIONS не изменён (явная проверка ключей/значений слоя A)."
|
||||
module: tests/test_plane_status_model.py
|
||||
covers: [AC-21]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-23
|
||||
type: unit
|
||||
description: "QG_CHECKS реестр и check_deploy_status контракты не изменены."
|
||||
module: tests/test_plane_status_model.py
|
||||
covers: [AC-22]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-24
|
||||
type: integration
|
||||
description: "Полный прогон pytest tests/ -q зелёный (регрессия)."
|
||||
module: tests/
|
||||
covers: [AC-23]
|
||||
expected: PASS
|
||||
287
docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md
Normal file
287
docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# ADR-001: Осмысленная статусная модель Plane (слой B)
|
||||
|
||||
**Work Item:** ORCH-066
|
||||
**Стадия:** architecture
|
||||
**Автор:** Architect
|
||||
**Дата:** 2026-06-07
|
||||
**Статус:** Accepted
|
||||
|
||||
> Контракт резолвера, алиасинга и разводки точек простановки статуса. Опирается на
|
||||
> BRD (`01-brd.md`), ТЗ (`02-trz.md`), критерии приёмки (`03-acceptance-criteria.md`).
|
||||
> Инфра-предусловие (статусы, создаваемые оператором) — `07-infra-requirements.md`,
|
||||
> риски — `10-tech-risks.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Контекст
|
||||
|
||||
Plane-доска оркестратора семантически перегружена: `In Progress` одновременно
|
||||
означает «человек запускает конвейер», «идёт анализ», «идёт прод-деплой» и «возврат
|
||||
из Needs Input». Оператор не различает реальный этап задачи → риск ошибочного ручного
|
||||
перевода статуса. ORCH-059 уже разгрузил `Approved` отдельным `Confirm Deploy`;
|
||||
ORCH-066 завершает наведение порядка по утверждённой Owner модели.
|
||||
|
||||
**Жёсткое разделение двух слоёв (инвариант проекта):**
|
||||
|
||||
| Слой | Что | Источник | ORCH-066 |
|
||||
|------|-----|----------|----------|
|
||||
| **A** | `STAGE_TRANSITIONS` — машина стадий | `src/stages.py` | **НЕ трогаем** |
|
||||
| **B** | Plane-статусы — индикация на доске | `src/plane_sync.py` + точки простановки | **меняем только это** |
|
||||
|
||||
Статус — **индикация, не управление**. Машинные вердикты по-прежнему читаются только
|
||||
из YAML-frontmatter артефактов (канон гейтов). Конвейер движут гейты слоя A; смена
|
||||
Plane-статуса не может продвинуть/откатить задачу (кроме существующих человеческих
|
||||
триггеров `To Analyse`/`Approved`/`Rejected`, которые и раньше были входами).
|
||||
|
||||
Целевая модель Owner:
|
||||
|
||||
```
|
||||
Backlog → Todo → [To Analyse] → Analysis → [In Review → Approved] → Architecture →
|
||||
Development → Code-Review → Testing → Awaiting Deploy → [Confirm Deploy] → Deploying →
|
||||
Monitoring after Deploy → Done
|
||||
```
|
||||
`[...]` = действие человека (вход-триггер); остальное ставит орк (индикация).
|
||||
Ветки: **Rejected** (откат), **Needs Input** (только аналитик), **Blocked** (затык/фейл
|
||||
деплоя/деградация), **Cancelled** (человек отменил задачу).
|
||||
|
||||
---
|
||||
|
||||
## 2. Решение
|
||||
|
||||
### 2.1. Реестр логических статусов (`src/plane_sync.py`)
|
||||
|
||||
Вводим 6 новых **логических ключей**. Имена в `_PLANE_NAME_TO_KEY` (резолв по имени из
|
||||
Plane API):
|
||||
|
||||
| Логический ключ | Plane name | Назначение |
|
||||
|-----------------|-----------|------------|
|
||||
| `to_analyse` | `To Analyse` | Вход-триггер: старт нового конвейера **и** resume аналитика из Needs Input. |
|
||||
| `analysis` | `Analysis` | Индикация стадии analysis (орк). |
|
||||
| `code_review` | `Code-Review` | Индикация стадии review (орк). Заменяет `review` как видимый статус. |
|
||||
| `awaiting_deploy` | `Awaiting Deploy` | Phase A approval-pending (орк). |
|
||||
| `deploying` | `Deploying` | Phase B прод-деплой идёт (орк). |
|
||||
| `monitoring` | `Monitoring after Deploy` | Phase C / post-deploy окно (орк). |
|
||||
|
||||
Существующие ключи сохраняются: `backlog`, `todo`, `in_progress`, `needs_input`,
|
||||
`in_review`, `blocked`, `done`, `cancelled`, `architecture`, `development`, `review`,
|
||||
`testing`, `approved`, `rejected`. `Cancelled` уже присутствует.
|
||||
|
||||
### 2.2. Fail-closed резолюция — **project-relative alias-fallback** (КРИТИЧНО, BR-12)
|
||||
|
||||
ТЗ §2.2 предложил статические алиасы на enduro-UUID в `_DEFAULT_STATES`. Архитектурное
|
||||
уточнение: для **частично сконфигурированного** проекта (оператор создал не все новые
|
||||
статусы) статический enduro-UUID в orchestrator-проекте даст невалидный `state` → PATCH
|
||||
422/404. Поэтому деградация делается **относительно того же проекта**, а не на чужой
|
||||
UUID.
|
||||
|
||||
**Два уровня fallback в `get_project_states()` (success-path), строго в порядке:**
|
||||
|
||||
1. Резолв по имени из Plane API (как сейчас).
|
||||
2. **Alias-fallback (новый):** для каждого отсутствующего нового ключа — UUID его
|
||||
**базового ключа из этого же проекта**:
|
||||
|
||||
```python
|
||||
_STATE_ALIAS_FALLBACK = {
|
||||
"to_analyse": "in_progress",
|
||||
"analysis": "in_progress",
|
||||
"code_review": "review",
|
||||
"awaiting_deploy": "in_review",
|
||||
"deploying": "in_progress",
|
||||
"monitoring": "done",
|
||||
}
|
||||
# после резолва по имени, ДО _DEFAULT_STATES.setdefault:
|
||||
for new_key, base_key in _STATE_ALIAS_FALLBACK.items():
|
||||
if new_key not in resolved and resolved.get(base_key):
|
||||
resolved[new_key] = resolved[base_key]
|
||||
```
|
||||
3. `_DEFAULT_STATES.setdefault(...)` (как сейчас) — последний резерв для путей, где
|
||||
API недоступен целиком (`if not project_id: return _DEFAULT_STATES`, полный провал
|
||||
запроса). В `_DEFAULT_STATES` новые ключи ТОЖЕ добавляются (= enduro-UUID базового
|
||||
ключа), чтобы любой caller всегда получал полный словарь и `states[key]` не кидал
|
||||
`KeyError`.
|
||||
|
||||
**Эффект деградации:**
|
||||
|
||||
| Сценарий | Поведение |
|
||||
|----------|-----------|
|
||||
| Orchestrator: все новые статусы созданы | резолв по имени → новые UUID (целевая модель). |
|
||||
| Orchestrator: создана ЧАСТЬ новых статусов | отсутствующие → **собственный** базовый UUID проекта → индикация деградирует до текущего статуса, PATCH валиден. |
|
||||
| Enduro (новые статусы не создаются никогда) | alias-fallback → собственные enduro базовые UUID → строго прежнее поведение (`In Progress`/`Review`/`Done`). |
|
||||
| Plane API down целиком | `_DEFAULT_STATES` (enduro-UUID) — без регресса относительно сегодняшнего поведения. |
|
||||
|
||||
Это паттерн ORCH-059 AC-7, усиленный project-relative разрешением. Все `set_issue_*` и
|
||||
`_set_issue_state_direct` остаются **never-raise** (PATCH-исключение логируется, не
|
||||
пробрасывается) — индикация деградирует, слой A не затрагивается.
|
||||
|
||||
### 2.3. Маппинг стадия → статус
|
||||
|
||||
- `_STAGE_TO_STATE_KEY` (живой путь `update_issue_state`→`stage_to_state`):
|
||||
`analysis` → `analysis` (было `in_progress`); `review` → `code_review` (было `review`).
|
||||
`deploy` остаётся `in_progress` (управляется Phase A/B/C напрямую). Остальные — без
|
||||
изменений.
|
||||
- `STAGE_VISIBILITY_STATE`: `review` → `code_review`; добавить `analysis` → `analysis`
|
||||
(для консистентности; `set_issue_stage_state` сейчас dormant, но карта обновляется).
|
||||
- `STAGE_TO_STATE` (legacy/test-only) — обновить `analysis`→`_DEFAULT_STATES["analysis"]`,
|
||||
`review`→`_DEFAULT_STATES["code_review"]`. UUID-значения **байт-в-байт прежние** (это
|
||||
алиасы на те же in_progress/review UUID) → тесты на конкретные UUID не краснеют.
|
||||
|
||||
### 2.4. Новые хелперы `src/plane_sync.py`
|
||||
|
||||
Тонкие обёртки по образцу `set_issue_in_review` (per-project резолв + `_set_issue_state_direct`,
|
||||
never-raise):
|
||||
|
||||
- `set_issue_analysis(work_item_id, project_id=None)`
|
||||
- `set_issue_code_review(work_item_id, project_id=None)`
|
||||
- `set_issue_awaiting_deploy(work_item_id, project_id=None)`
|
||||
- `set_issue_deploying(work_item_id, project_id=None)`
|
||||
- `set_issue_monitoring(work_item_id, project_id=None)`
|
||||
|
||||
`get_project_states` всегда возвращает полный словарь (см. §2.2), поэтому `[key]` не
|
||||
кидает `KeyError`.
|
||||
|
||||
### 2.5. Точки простановки статуса (разводка)
|
||||
|
||||
| Файл:место | Сейчас | Должно стать | AC |
|
||||
|------------|--------|--------------|----|
|
||||
| `webhooks/plane.py` `handle_issue_updated` | `new_state == in_progress` → `handle_status_start` | `new_state == to_analyse` → `handle_status_start` (при алиасе совпадает с `in_progress`) | AC-1, AC-17 |
|
||||
| `webhooks/plane.py` `start_pipeline` (успешный старт) | статус остаётся `In Progress` | в конце старта орк ставит `set_issue_analysis` | AC-3 |
|
||||
| `webhooks/plane.py` `handle_status_start` (resume-ветка) | relaunch агента стадии | при relaunch орк ставит `set_issue_analysis`; fork «старт vs resume» (`get_task_by_plane_id` + `has_active_job_for_task`) — **без изменений** | AC-2, AC-4 |
|
||||
| `webhooks/plane.py` `_rollback_stage` (reject@analysis, ~583) | `set_issue_in_progress` | `set_issue_analysis` | AC-3 |
|
||||
| `stage_engine.py` `_handle_analysis_approved_flow` (artifacts ready) | `set_issue_in_review` | **без изменений** (BR-9) | AC-13 |
|
||||
| `stage_engine.py` `_handle_analysis_approved_flow` (questions) | `set_issue_needs_input` | **без изменений** (BR-10) | AC-14 |
|
||||
| `stage_engine.py` rollback@analysis (architect conflict, ~669) | `set_issue_in_progress` | `set_issue_analysis` | AC-3 |
|
||||
| `stage_engine.py` `_handle_self_deploy_phase_a` (~1012) | `set_issue_in_review` | `set_issue_awaiting_deploy` | AC-6, AC-13 |
|
||||
| `stage_engine.py` `_handle_self_deploy_phase_b` (после `INITIATED` marker) | статус не меняет | `set_issue_deploying` | AC-7 |
|
||||
| `stage_engine.py` terminal-sync `deploy → done` (~338) | `set_issue_done` для всех | **self (`post_deploy_applies`):** `set_issue_monitoring`; **не-self:** `set_issue_done` как сейчас | AC-8, AC-9 |
|
||||
| `stage_engine.py` `run_post_deploy_monitor` HEALTHY+окно закрыто (~1260) | статус не трогает | `set_issue_done` (явно) | AC-10 |
|
||||
| `stage_engine.py` `run_post_deploy_monitor` DEGRADED (~1273) | alert/log | `set_issue_blocked` (+ существующий ALERT_ONLY) | AC-11 |
|
||||
|
||||
**Разводка terminal-sync (детально, AC-8/AC-9).** Текущий код безусловно зовёт
|
||||
`set_issue_done` на `next_stage == "done"`, затем (для self) армит post-deploy monitor.
|
||||
Разводим по `post_deploy.post_deploy_applies(repo)`:
|
||||
|
||||
```python
|
||||
if next_stage == "done" and work_item_id:
|
||||
if post_deploy.post_deploy_applies(repo):
|
||||
set_issue_monitoring(work_item_id) # self: окно наблюдения, НЕ Done сразу
|
||||
else:
|
||||
set_issue_done(work_item_id) # не-self: терминальный Done как сейчас
|
||||
# арм монитора (существующий блок ~361) — без изменений
|
||||
```
|
||||
Финальный `Done`/`Blocked` для self-hosting перекладывается на `run_post_deploy_monitor`.
|
||||
При деградированном алиасе `monitoring==done` self-hosting показывает `Done` и затем
|
||||
монитор держит `Done`/флипает `Blocked` — поведение идентично сегодняшнему.
|
||||
|
||||
**AC-12 (инвариант ORCH-021):** добавление `set_issue_blocked` в DEGRADED-ветку —
|
||||
**только индикация**; тик по-прежнему НИКОГДА не рестартит/откатывает прод-контейнер
|
||||
(self-hosting остаётся `ALERT_ONLY`). `set_issue_blocked` — Plane-PATCH, не действие над
|
||||
контейнером.
|
||||
|
||||
**Cancelled (AC-15):** изменений кода НЕ требует. `handle_issue_updated` реагирует только
|
||||
на `to_analyse`/`approved`/`rejected`; `Cancelled` падает в `else` → «no pipeline action».
|
||||
Орк не делает advance/rollback — индикация, не управление. Критерий выполнен существующим
|
||||
кодом.
|
||||
|
||||
### 2.6. Reconciler (`src/reconciler.py`)
|
||||
|
||||
- **F-2 `_reconcile_plane_project`:** заменить триггер `in_progress` → `to_analyse` в
|
||||
списке запрашиваемых статусов (`list_issues_by_state([to_analyse, approved, rejected])`)
|
||||
и в `_reconcile_plane_issue` маршрутизировать `new_state == to_analyse` →
|
||||
`handle_status_start`. При алиасе `to_analyse == in_progress` (enduro) поведение
|
||||
идентично текущему (один UUID; `list_issues_by_state` дедуплицирует через `set`). AC-19.
|
||||
- **Guard 2 `_is_blocked_or_needs_input`:** расширить skip-множество активными ожиданиями
|
||||
`awaiting_deploy`/`deploying`/`monitoring` (BR-13, AC-20). **Анти-регресс enduro
|
||||
(КРИТИЧНО):** новые ключи алиасятся на `in_review`/`in_progress`/`done`; добавить их в
|
||||
skip «как есть» → на enduro `In Progress`/`Done`-задачи начнут ошибочно пропускаться
|
||||
F-1 (регресс ORCH-053/060). Поэтому активные ожидания включаются в skip **только когда
|
||||
они РАЗЛИЧНЫ от базовых рабочих статусов проекта** (т.е. реально созданы):
|
||||
|
||||
```python
|
||||
base_working = {states.get(k) for k in (
|
||||
"backlog","todo","in_progress","in_review","review",
|
||||
"architecture","development","testing","approved","rejected","done")}
|
||||
extra_waits = {states.get("awaiting_deploy"),
|
||||
states.get("deploying"),
|
||||
states.get("monitoring")} - base_working - {None}
|
||||
skip_set = {states.get("blocked"), states.get("needs_input")} | extra_waits
|
||||
return cur in skip_set
|
||||
```
|
||||
Enduro (алиасы схлопываются в base) → `extra_waits == {}` → нулевой регресс. Orchestrator
|
||||
(отдельные UUID) → три реальных статуса в skip → BR-13. Семантику метода обобщаем до
|
||||
«human-or-active-wait»; флаг `reconcile_skip_blocked_enabled` продолжает гасить этот
|
||||
networked-чек. F-1 и так структурно не оживляет эти состояния (Phase A: `check_deploy_status`
|
||||
red → silent; Deploying: active finalizer job → active-job guard; Monitoring: стадия
|
||||
`done` → не итерируется) — Guard 2 это defense-in-depth по требованию Owner.
|
||||
|
||||
### 2.7. Без kill-switch
|
||||
|
||||
Отдельный env-флаг новой модели **не вводится**. Раскат естественно гейтится
|
||||
**инфра-предусловием**: пока оператор не создал новые статусы — alias-fallback (§2.2)
|
||||
держит строго прежнее поведение; создал — резолв по имени включает новую модель. Это
|
||||
проще отдельного флага и соответствует принципу «минимум зависимостей». (ТЗ §6 допускает
|
||||
флаг как опциональный — сознательно отказываемся.)
|
||||
|
||||
---
|
||||
|
||||
## 3. Затронутые модули (карта изменений)
|
||||
|
||||
| Модуль | Изменение |
|
||||
|--------|-----------|
|
||||
| `src/plane_sync.py` | `_PLANE_NAME_TO_KEY` +6; `_DEFAULT_STATES` +6 (enduro-alias UUID); `_STATE_ALIAS_FALLBACK` (новое) + применение в `get_project_states`; `_STAGE_TO_STATE_KEY` (analysis/review); `STAGE_VISIBILITY_STATE`; `STAGE_TO_STATE` (legacy); 5 новых `set_issue_*`. |
|
||||
| `src/webhooks/plane.py` | триггер `in_progress`→`to_analyse` в `handle_issue_updated`; `set_issue_analysis` в `start_pipeline` и resume-ветке `handle_status_start`; `_rollback_stage` reject@analysis → `set_issue_analysis`. |
|
||||
| `src/stage_engine.py` | Phase A → `set_issue_awaiting_deploy`; Phase B → `set_issue_deploying`; terminal-sync split (`monitoring` vs `done`); post-deploy monitor HEALTHY→`set_issue_done`, DEGRADED→`set_issue_blocked`; rollback@analysis (architect conflict) `set_issue_in_progress`→`set_issue_analysis`. |
|
||||
| `src/reconciler.py` | F-2 триггер `to_analyse`; Guard 2 skip-set + анти-регресс subtraction. |
|
||||
| `src/stages.py` | **НЕ трогаем** (инвариант слоя A). |
|
||||
| `src/config.py` | Без изменений (kill-switch не вводится). |
|
||||
|
||||
---
|
||||
|
||||
## 4. Инварианты (проверяемые, AC-21/AC-22)
|
||||
|
||||
- `src/stages.py` `STAGE_TRANSITIONS` — diff пуст (байт-в-байт).
|
||||
- `QG_CHECKS`, `check_deploy_status`/`_parse_deploy_status`, exit-коды хука (0/1/2),
|
||||
merge-gate, `check_branch_mergeable`/`check_staging_image_fresh`, схема БД — без изменений.
|
||||
- `Confirm Deploy` (ORCH-059), механизм `Needs Input` (analyst-only) — без изменений.
|
||||
- Новых HTTP-эндпоинтов нет; `GET /queue`/`GET /status` контракт без изменений.
|
||||
- Миграций БД нет (`tasks` не хранит Plane-статус; источник истины — стадия в БД + Plane API).
|
||||
- Все новые `set_issue_*` / резолв — never-raise.
|
||||
- Не-self (enduro) терминальный `deploy → Done` — без регресса.
|
||||
|
||||
---
|
||||
|
||||
## 5. Последствия
|
||||
|
||||
**Плюсы**
|
||||
- Доска читаема: каждый этап = осмысленный статус; человеческие входы визуально отделены
|
||||
от индикации.
|
||||
- `In Progress` разгружен: больше не «всё подряд».
|
||||
- Fail-closed усилен (project-relative): частичная конфигурация не ломает ни индикацию,
|
||||
ни конвейер.
|
||||
- Слой A нетронут → нулевой риск для машины стадий и гейтов всех проектов (self-hosting).
|
||||
- Нет нового флага/таблицы → меньше движущихся частей.
|
||||
|
||||
**Минусы / ограничения**
|
||||
- Требуется ручное инфра-действие оператора (создать 6 статусов в проекте ORCH) — до
|
||||
этого orchestrator деградирует до старой индикации (см. `07-infra-requirements.md`).
|
||||
- Статусы кэшируются per-process (`_STATES_CACHE`): после создания статусов нужен
|
||||
`reload_project_states()` или рестарт **staging** (не прод — см. self-hosting риск).
|
||||
- Guard-2 subtraction добавляет немного логики; покрывается тестами (enduro-алиас → пустой
|
||||
extra; orchestrator → три статуса).
|
||||
|
||||
**Self-hosting (⚠️):** изменения — слой B (Plane-индикация) + reconciler-гварды; машина
|
||||
стадий и контракты деплоя нетронуты. Выкладка ОБЯЗАТЕЛЬНО через `deploy-staging` (8501)
|
||||
до прод-деплоя орка. Прод-контейнер не рестартить в рамках задачи вне штатного staging-гейта.
|
||||
|
||||
---
|
||||
|
||||
## 6. Альтернативы (отклонены)
|
||||
|
||||
- **Статический enduro-UUID алиас (ТЗ §2.2 буквально):** ломается на частичной
|
||||
конфигурации orchestrator-проекта (чужой UUID → PATCH 422). Заменён project-relative
|
||||
alias-fallback (§2.2).
|
||||
- **Глобальный env kill-switch новой модели:** избыточен — инфра-предусловие уже даёт
|
||||
естественный гейт раската (§2.7).
|
||||
- **Хранить Plane-статус в `tasks` (миграция БД):** не нужно; источник истины — стадия +
|
||||
живой Plane API. Нарушило бы инвариант «без лишних зависимостей».
|
||||
- **Менять `STAGE_TRANSITIONS` ради новых статусов:** запрещено (инвариант слоя A);
|
||||
статусы — индикация, отделены от машины стадий.
|
||||
96
docs/work-items/ORCH-066/07-infra-requirements.md
Normal file
96
docs/work-items/ORCH-066/07-infra-requirements.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# 07 — Требования к инфраструктуре
|
||||
|
||||
**Work Item:** ORCH-066
|
||||
**Автор:** Architect
|
||||
**Дата:** 2026-06-07
|
||||
|
||||
> ORCH-066 не меняет топологию (контейнеры/порты/сеть — без изменений, см.
|
||||
> `docs/operations/INFRA.md`). Единственное инфра-действие — создание новых
|
||||
> Plane-статусов в проекте **ORCH** руками оператора через Plane API. Это
|
||||
> **предусловие эксплуатации**, не часть кодового PR.
|
||||
|
||||
---
|
||||
|
||||
## 1. Что нужно сделать оператору (ДО эксплуатации новой модели)
|
||||
|
||||
Создать в Plane-проекте **ORCH** следующие статусы (states) с точными именами —
|
||||
резолвер сопоставляет их по `name` (`_PLANE_NAME_TO_KEY`):
|
||||
|
||||
| Plane name (точно) | Логический ключ | Группа Plane (рекомендуемая) | Назначение |
|
||||
|--------------------|-----------------|------------------------------|------------|
|
||||
| `To Analyse` | `to_analyse` | unstarted / started | Человеческий вход: старт конвейера + resume аналитика из Needs Input. |
|
||||
| `Analysis` | `analysis` | started | Индикация стадии анализа. |
|
||||
| `Code-Review` | `code_review` | started | Индикация стадии review. |
|
||||
| `Awaiting Deploy` | `awaiting_deploy` | started | Phase A: ожидание ручного approve на прод-деплой. |
|
||||
| `Deploying` | `deploying` | started | Phase B: идёт прод-деплой. |
|
||||
| `Monitoring after Deploy` | `monitoring` | started | Phase C / окно пост-деплой наблюдения. |
|
||||
|
||||
`Confirm Deploy` (ORCH-059) и базовые статусы (`Backlog`, `Todo`, `In Progress`,
|
||||
`Architecture`, `Development`, `Review`, `Testing`, `Approved`, `Rejected`, `Done`,
|
||||
`Cancelled`, `Needs Input`, `In Review`, `Blocked`) уже существуют — **не трогать**.
|
||||
|
||||
> ⚠️ **Точность имён критична.** Резолв идёт по строковому `name`. Опечатка/иной регистр
|
||||
> → статус не сопоставится → ключ деградирует на собственный базовый UUID проекта
|
||||
> (alias-fallback, ADR §2.2): индикация откатится к старому статусу, но конвейер
|
||||
> продолжит работать. Дефис в `Code-Review` — обязателен.
|
||||
|
||||
---
|
||||
|
||||
## 2. Plane API — как создать статус
|
||||
|
||||
Эндпоинт (как в `src/plane_sync.py`, `PLANE_BASE = {plane_api_url}/api/v1`):
|
||||
|
||||
```
|
||||
POST {PLANE_BASE}/workspaces/{WORKSPACE}/projects/{ORCH_PROJECT_ID}/states/
|
||||
Headers: X-API-Key: <PLANE_API_TOKEN> (или соответствующий бот-токен с правами)
|
||||
Body (JSON):
|
||||
{ "name": "To Analyse", "group": "started", "color": "#3f76ff" }
|
||||
```
|
||||
|
||||
Повторить для каждого имени из таблицы §1. `group` влияет только на колонку доски;
|
||||
оркестратор `group` не читает (резолв строго по `name`). `color` — на вкус оператора.
|
||||
|
||||
Проверка после создания:
|
||||
|
||||
```
|
||||
GET {PLANE_BASE}/workspaces/{WORKSPACE}/projects/{ORCH_PROJECT_ID}/states/
|
||||
```
|
||||
В ответе должны присутствовать все 6 имён.
|
||||
|
||||
---
|
||||
|
||||
## 3. Сброс кэша статусов (важно)
|
||||
|
||||
`get_project_states` кэширует резолв per-process (`_STATES_CACHE`). После создания
|
||||
статусов оркестратор подхватит их **только** после сброса кэша:
|
||||
|
||||
- штатно — `plane_sync.reload_project_states(project_id)` (или рестарт процесса);
|
||||
- на **staging** (8501) — безопасный рестарт песочницы;
|
||||
- на **прод** (8500) — **НЕ рестартить контейнер ради этого** в рамках задачи
|
||||
(self-hosting: общий контейнер всех проектов). Кэш заполняется при первом обращении к
|
||||
проекту; если статусы созданы ДО первого PATCH в цикле новой версии — отдельный сброс не
|
||||
нужен. Если созданы позже — дождаться штатного цикла обновления/деплоя орка.
|
||||
|
||||
---
|
||||
|
||||
## 4. Порядок раската (рекомендация)
|
||||
|
||||
1. Слить кодовый PR ORCH-066 через `deploy-staging` (8501).
|
||||
2. Создать 6 статусов в проекте ORCH (§1–§2).
|
||||
3. Сбросить кэш / поднять staging, прогнать sandbox-задачу — убедиться, что доска
|
||||
показывает `Analysis` / `Code-Review` / `Awaiting Deploy` / `Deploying` /
|
||||
`Monitoring after Deploy` / `Done` на соответствующих этапах.
|
||||
4. Прод-деплой орка штатным self-deploy (Phase A → approve → Phase B/C).
|
||||
|
||||
**До шага 2** система работает строго как до ORCH-066 (alias-fallback) — раскат
|
||||
безопасно обратим: не создавать/удалить статусы = откат индикации к старой модели,
|
||||
без изменения кода.
|
||||
|
||||
---
|
||||
|
||||
## 5. Что НЕ требуется
|
||||
|
||||
- Никаких изменений docker-compose, портов, сети, томов, `.env`/`.env.staging`.
|
||||
- Никаких миграций БД (`tasks` не хранит Plane-статус).
|
||||
- Никаких изменений в проекте **enduro-trails** — там новые статусы не создаются;
|
||||
alias-fallback держит прежнюю индикацию (`In Progress`/`Review`/`Done`).
|
||||
31
docs/work-items/ORCH-066/10-tech-risks.md
Normal file
31
docs/work-items/ORCH-066/10-tech-risks.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# 10 — Технические риски
|
||||
|
||||
**Work Item:** ORCH-066
|
||||
**Автор:** Architect
|
||||
**Дата:** 2026-06-07
|
||||
|
||||
Риски слоя B (Plane-индикация). Слой A (`STAGE_TRANSITIONS`/гейты) не затрагивается, поэтому
|
||||
класс «сломали конвейер» структурно исключён — худший исход любого риска ниже = неверная
|
||||
**индикация**, не остановка конвейера.
|
||||
|
||||
| ID | Риск | Вероятность | Влияние | Митигация |
|
||||
|----|------|-------------|---------|-----------|
|
||||
| **R1** | Частичная конфигурация: оператор создал не все 6 статусов в ORCH → отсутствующий ключ деградирует. Наивный статический enduro-UUID дал бы невалидный `state` (PATCH 422) на orchestrator-issue. | Средняя | Средн. | **Project-relative alias-fallback** (ADR §2.2): отсутствующий ключ → собственный базовый UUID проекта → PATCH валиден, индикация откатывается к текущему статусу. Покрыть тестом partial-config. |
|
||||
| **R2** | Enduro-регресс через Guard 2: новые ключи алиасятся на `in_progress`/`in_review`/`done`; наивное добавление в skip-set заставит F-1 пропускать enduro `In Progress`/`Done` → сломанная реконсиляция (ORCH-053/060). | Средняя | Высок. | **Subtraction базовых рабочих статусов** (ADR §2.6): `extra_waits -= base_working`. На enduro (алиасы схлопнуты) `extra_waits == {}` → нулевой регресс. Тест: enduro-алиас не добавляет skip, orchestrator-distinct добавляет. |
|
||||
| **R3** | Двойной триггер старта: F-2 reconciler и webhook оба маршрутизируют `to_analyse`; при алиасе `to_analyse == in_progress` возможен повтор. | Низкая | Низк. | `list_issues_by_state` дедуплицирует UUID через `set`; active-job guard + atomic create-claim в `handle_status_start` (`get_task_by_plane_id` + `has_active_job_for_task`) — без двойного старта (AC-4). Сохранить fork как есть. |
|
||||
| **R4** | Кэш статусов: после создания статусов `_STATES_CACHE` отдаёт старый резолв до сброса → доска не обновляется. | Средняя | Низк. | `reload_project_states()` / рестарт **staging**. Документировано в `07-infra-requirements.md §3`. Прод-рестарт ради кэша — запрещён (self-hosting). |
|
||||
| **R5** | Опечатка в имени статуса оператором (`Code Review` без дефиса и т.п.) → ключ не резолвится. | Средняя | Низк. | Резолв по точному `name`; при промахе — alias-fallback (деградация, не падение). Точные имена и проверка в `07-infra-requirements.md §1–2`. |
|
||||
| **R6** | Terminal-sync split: ошибка ветвления `post_deploy_applies` → enduro получает `Monitoring after Deploy` вместо `Done` (регресс AC-9) или self уходит в `Done` минуя окно (AC-8). | Низкая | Средн. | Единый источник условности — `post_deploy.post_deploy_applies(repo)` (та же функция, что армит монитор). Тесты AC-8 (self→monitoring) и AC-9 (не-self→done). |
|
||||
| **R7** | Phase B: `set_issue_deploying` поставлен до фактического старта детача → ложная индикация при провале `initiate_deploy`. | Низкая | Низк. | Ставить `set_issue_deploying` **после** успешного `initiate_deploy` и записи `INITIATED` marker (ADR §2.5); провал `initiate_deploy` оставляет `Awaiting Deploy` + просьбу повторить approve. |
|
||||
| **R8** | Post-deploy DEGRADED → `set_issue_blocked` ошибочно трактуется как «действие над продом». | Низкая | Высок.(если) | `set_issue_blocked` — только Plane-PATCH. Тик остаётся `ALERT_ONLY`, НИКОГДА не рестартит/откатывает прод-контейнер (AC-12, ORCH-021 BR-5). Явный тест: self DEGRADED не трогает контейнер. |
|
||||
| **R9** | Plane API недоступен в момент простановки статуса → PATCH падает. | Низкая | Низк. | Все `set_issue_*`/`_set_issue_state_direct` — never-raise (логируют, не пробрасывают). Индикация пропускается, слой A не затронут. |
|
||||
| **R10** | Регресс на тестах, читающих `STAGE_TO_STATE`/`PLANE_STATES` конкретные UUID. | Низкая | Низк. | Новые ключи в `_DEFAULT_STATES` = алиасы на те же in_progress/review/done UUID → значения байт-в-байт; `STAGE_TO_STATE` analysis/review остаются прежними UUID (ADR §2.3). |
|
||||
| **R11** | Self-hosting: выкладка орка минуя staging. | Низкая | Высок. | Обязательный `deploy-staging` гейт (8501); прод не рестартить вне штатного self-deploy. Раскат обратим (не создавать статусы = старое поведение). |
|
||||
|
||||
## Сводный вывод
|
||||
|
||||
Все риски снижаемы в рамках принятой архитектуры; ни один не способен остановить конвейер
|
||||
(слой A инвариантен). Два ключевых требуют аккуратной реализации и обязательных тестов:
|
||||
**R1** (project-relative alias-fallback) и **R2** (Guard-2 anti-regress subtraction) —
|
||||
оба зафиксированы в ADR §2.2 и §2.6 как явные контракты. Эскалации `arch:major-change` не
|
||||
требуется: изменение локализовано в слое B, без новых компонентов/стадий/QG/миграций.
|
||||
89
docs/work-items/ORCH-066/12-review.md
Normal file
89
docs/work-items/ORCH-066/12-review.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-066
|
||||
verdict: APPROVED
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-066
|
||||
|
||||
## Summary
|
||||
Осмысленная статусная модель Plane (слой B — индикация). Реализация затрагивает
|
||||
строго слой B (`src/plane_sync.py`, точки простановки в `src/stage_engine.py` /
|
||||
`src/webhooks/plane.py` / `src/reconciler.py`) и **не трогает слой A**
|
||||
(`src/stages.py::STAGE_TRANSITIONS` — diff пуст). Все 4 оси проверки (ТЗ, ADR,
|
||||
качество кода, тесты) и проверка документации — пройдены. `pytest tests/ -q`:
|
||||
**774 passed**. Вердикт — **APPROVED**.
|
||||
|
||||
## Соответствие ТЗ (02-trz.md)
|
||||
- §2.1 — 6 новых логических ключей в `_PLANE_NAME_TO_KEY` + `_DEFAULT_STATES`. ✔
|
||||
- §2.2 — fail-closed резолюция (BR-12). ✔ (реализована усиленная project-relative
|
||||
версия — см. ADR ниже).
|
||||
- §2.3 — `_STAGE_TO_STATE_KEY` (analysis→analysis, review→code_review),
|
||||
`STAGE_VISIBILITY_STATE`, legacy `STAGE_TO_STATE` (UUID байт-в-байт прежние). ✔
|
||||
- §2.4 — точки простановки разведены (handle_issue_updated триггер `to_analyse`,
|
||||
start_pipeline/resume → Analysis, Phase A → Awaiting Deploy, Phase B → Deploying,
|
||||
terminal-sync split, post-deploy HEALTHY→Done / DEGRADED→Blocked,
|
||||
rollback@analysis → Analysis). ✔
|
||||
- §2.5 — 5 новых never-raise хелперов `set_issue_*`. ✔
|
||||
- §3 — reconciler F-2 триггер `to_analyse` (+ resume-ветка), Guard 2 skip-set с
|
||||
вычитанием base_working. ✔
|
||||
- §4/§5/§6 — нет новых эндпоинтов, нет миграций БД, `QG_CHECKS` не расширен. ✔
|
||||
|
||||
## Соответствие ADR (06-adr/ADR-001)
|
||||
- §2.2 project-relative alias-fallback (`_STATE_ALIAS_FALLBACK`, применён ДО
|
||||
`_DEFAULT_STATES.setdefault`) — реализован точно по контракту, деградация на
|
||||
собственный базовый UUID проекта, PATCH остаётся валидным на частичной
|
||||
конфигурации. ✔
|
||||
- §2.5 terminal-sync split по `post_deploy.post_deploy_applies(repo)` — реализован
|
||||
как в ADR (self → Monitoring, не-self → Done). ✔
|
||||
- §2.6 Guard 2 анти-регресс (extra_waits − base_working − {None}) — реализован
|
||||
дословно, enduro-алиасы схлопываются → нулевой регресс. ✔
|
||||
- §2.7 без kill-switch — config.py не изменён (diff пуст). ✔
|
||||
|
||||
## Качество кода
|
||||
- Все новые `set_issue_*` следуют образцу `set_issue_in_review` (per-project резолв
|
||||
+ `_set_issue_state_direct`), контракт never-raise сохранён, есть docstrings. ✔
|
||||
- Post-deploy/terminal-sync простановки обёрнуты в try/except с warning-логом
|
||||
(never break the tick). ✔
|
||||
- Переменные в scope корректны (`work_item_id` определён до всех новых вызовов в
|
||||
`start_pipeline`/`handle_status_start`/stage_engine). ✔
|
||||
- AC-12 соблюдён: `set_issue_blocked` в DEGRADED-ветке — только индикация, тик
|
||||
прод-контейнер не трогает. ✔
|
||||
|
||||
## Качество тестов
|
||||
- Содержательные, не тривиальные: `test_plane_status_failclosed.py`
|
||||
(TC-16/17/18 — partial project, API down, never-raise сеттеров, enduro alias
|
||||
старт), `test_plane_to_analyse_resume.py`, `test_plane_status_model.py`,
|
||||
`test_deploy_terminal_sync.py` (self/не-self split), `test_post_deploy_integration.py`,
|
||||
`test_reconciler*.py` (F-2 to_analyse + Guard 2). ✔
|
||||
|
||||
## Инварианты (AC-21/AC-22)
|
||||
- `src/stages.py` — diff 0 строк (STAGE_TRANSITIONS байт-в-байт). ✔
|
||||
- `src/qg/checks.py` — diff 0 строк (QG_CHECKS, check_deploy_status). ✔
|
||||
- `src/config.py` — diff 0 строк. ✔
|
||||
- Схема БД — без миграций. ✔
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- нет
|
||||
|
||||
### P1 — Must fix
|
||||
- нет
|
||||
|
||||
### P2 — Should fix
|
||||
- нет
|
||||
|
||||
## Документация
|
||||
Обновлена в том же PR (golden source соблюдён):
|
||||
- `CLAUDE.md` — добавлена секция «Статусная модель Plane (ORCH-066)». ✔
|
||||
- `docs/architecture/README.md` — секция «Осмысленная статусная модель Plane
|
||||
(ORCH-066)» + обновлён статусный footer. ✔
|
||||
- `CHANGELOG.md` — подробная запись в [Unreleased]/Added. ✔
|
||||
- `06-adr/ADR-001-plane-status-model.md` — заведён. ✔
|
||||
- `07-infra-requirements.md` — присутствует (инфра-предусловие: 6 Plane-статусов
|
||||
создаёт оператор). ✔
|
||||
|
||||
Изменения `src/` полностью отражены в документации → требование
|
||||
«документация обновлена при изменении src/» выполнено.
|
||||
77
docs/work-items/ORCH-066/13-test-report.md
Normal file
77
docs/work-items/ORCH-066/13-test-report.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-066
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-066
|
||||
|
||||
Осмысленная статусная модель Plane (слой B — индикация). Прогон полного регресса +
|
||||
покрытие тест-плана `04-test-plan.yaml` + проверка инвариантов слоя A.
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3
|
||||
- Ветка: feature/ORCH-066-plane
|
||||
- Дата: 2026-06-07
|
||||
|
||||
## Результаты по тест-плану (04-test-plan.yaml)
|
||||
|
||||
| TC ID | Покрывает | Описание | Модуль | Результат |
|
||||
|-------|-----------|----------|--------|-----------|
|
||||
| TC-01 | AC-1 | To Analyse без task → start_pipeline | test_status_trigger.py | PASS |
|
||||
| TC-02 | AC-2,BR-11 | To Analyse resume аналитика, без двойного task | test_plane_to_analyse_resume.py | PASS |
|
||||
| TC-03 | AC-3 | Старт/relaunch → статус Analysis | test_plane_status_model.py | PASS |
|
||||
| TC-04 | AC-4 | Busy-guard: active-job → не relaunch | test_plane_to_analyse_resume.py | PASS |
|
||||
| TC-05 | AC-5 | review → статус Code-Review | test_plane_status_model.py | PASS |
|
||||
| TC-06 | AC-6,AC-13 | Phase A → Awaiting Deploy (не In Review) | test_deploy_approve.py | PASS |
|
||||
| TC-07 | AC-7 | Phase B → Deploying | test_deploy_approve.py | PASS |
|
||||
| TC-08 | AC-8 | Phase C self → Monitoring after Deploy | test_deploy_terminal_sync.py | PASS |
|
||||
| TC-09 | AC-9 | Не-self deploy→done → Done (без регресса) | test_deploy_terminal_sync.py | PASS |
|
||||
| TC-10 | AC-10 | Post-deploy HEALTHY → Done | test_post_deploy.py | PASS |
|
||||
| TC-11 | AC-11 | Post-deploy DEGRADED → Blocked | test_post_deploy.py | PASS |
|
||||
| TC-12 | AC-12 | Self-тик не рестартит прод | test_post_deploy.py | PASS |
|
||||
| TC-13 | AC-13 | In Review только за approve-pending | test_analyst_status_only_regression.py | PASS |
|
||||
| TC-14 | AC-14,BR-10 | Needs Input без изменений | test_plane_status_model.py | PASS |
|
||||
| TC-15 | AC-15 | Cancelled → нет действий конвейера | test_plane_webhook.py | PASS |
|
||||
| TC-16 | AC-16,BR-12 | Fail-closed default-алиасы, нет исключений | test_plane_status_failclosed.py | PASS |
|
||||
| TC-17 | AC-16 | Plane API down → fallback, never-raise | test_plane_status_failclosed.py | PASS |
|
||||
| TC-18 | AC-17 | enduro In Progress стартует через алиас | test_plane_status_failclosed.py | PASS |
|
||||
| TC-19 | AC-18 | Резолв по имени → корректный UUID | test_orch10_states.py | PASS |
|
||||
| TC-20 | AC-19 | F-2 реконсилирует To Analyse | test_reconciler_plane.py | PASS |
|
||||
| TC-21 | AC-20,BR-13 | Guard 2 skip активных ожиданий | test_reconciler.py | PASS |
|
||||
| TC-22 | AC-21 | STAGE_TRANSITIONS не изменён | test_plane_status_model.py | PASS |
|
||||
| TC-23 | AC-22 | QG_CHECKS/check_deploy_status не изменены | test_plane_status_model.py | PASS |
|
||||
| TC-24 | AC-23 | Полный регресс pytest зелёный | tests/ | PASS |
|
||||
|
||||
Все 24 тест-кейса — PASS.
|
||||
|
||||
## Инварианты слоя A (AC-21 / AC-22)
|
||||
Diff против `origin/main` (merge-base `4815e378`):
|
||||
- `src/stages.py` (STAGE_TRANSITIONS) — diff пуст ✔
|
||||
- `src/qg/checks.py` (QG_CHECKS, check_deploy_status) — diff пуст ✔
|
||||
- `src/config.py` (без kill-switch) — diff пуст ✔
|
||||
|
||||
## Smoke test API (TestClient — прод-контейнер 8500 не трогался)
|
||||
> `curl` в окружении недоступен; smoke прогнан через FastAPI TestClient (lifespan),
|
||||
> без рестарта/обращения к прод-контейнеру (self-hosting safety).
|
||||
|
||||
| Endpoint | Статус | Тело (фрагмент) |
|
||||
|----------|--------|-----------------|
|
||||
| GET /health | 200 | `{"status":"ok","service":"orchestrator"}` |
|
||||
| GET /status | 200 | `{"active_tasks":[...]}` |
|
||||
| GET /queue | 200 | `{"counts":{...},"max_concurrency":1,...}` |
|
||||
|
||||
## Вывод pytest
|
||||
```
|
||||
======================= 774 passed, 1 warning in 17.68s ========================
|
||||
```
|
||||
(единственный warning — PydanticDeprecatedSince20 в src/config.py, предсуществующий,
|
||||
не связан с ORCH-066)
|
||||
|
||||
Прогон по модулям тест-плана: `117 passed` (ORCH-066-специфичные файлы).
|
||||
|
||||
## Итог
|
||||
PASS — все тесты зелёные (774 passed), все 24 TC покрыты, инварианты слоя A
|
||||
сохранены (diff пуст), smoke-эндпоинты отвечают 200. Review-вердикт APPROVED.
|
||||
Задача готова к переходу на стадию deploy-staging.
|
||||
12
docs/work-items/ORCH-066/14-deploy-log.md
Normal file
12
docs/work-items/ORCH-066/14-deploy-log.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
deploy_status: SUCCESS
|
||||
work_item: ORCH-066
|
||||
hook_exit_code: 0
|
||||
deployed_by: deploy-finalizer
|
||||
---
|
||||
|
||||
# Deploy log — ORCH-036 executable self-deploy
|
||||
|
||||
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
|
||||
|
||||
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.
|
||||
39
docs/work-items/ORCH-066/15-staging-log.md
Normal file
39
docs/work-items/ORCH-066/15-staging-log.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-07T22:01:57Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed against the live `orchestrator-staging` instance (port 8501),
|
||||
run canonically via `docker exec orchestrator-staging python3 /repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub`.
|
||||
|
||||
**Result: 8/10 checks PASS — exit code 0 (advance).**
|
||||
|
||||
All REAL (pipeline) checks are green. The two failing checks are the known
|
||||
SANDBOX_INFRA-only checks C9a/C9b (sandbox branch / analyst-job — depend on
|
||||
SANDBOX bot accounts being project members, not on the pipeline), which are
|
||||
waived under ORCH-061 since every REAL check passed.
|
||||
|
||||
```
|
||||
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
|
||||
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
|
||||
```
|
||||
|
||||
## Check breakdown
|
||||
|
||||
| Block | Check | Result |
|
||||
|-------|-------|--------|
|
||||
| A SMOKE | A1 GET /health → 200 status=ok | PASS |
|
||||
| A SMOKE | A2 GET /queue → 200 with counts/max_concurrency/resilience | PASS |
|
||||
| A SMOKE | A3 ORCH_STAGING=true (not prod) | PASS |
|
||||
| B ACCESS | B4 Plane: sandbox project accessible | PASS |
|
||||
| B ACCESS | B5 Gitea: orchestrator-sandbox accessible, push=true | PASS |
|
||||
| B ACCESS | B6 Registry: sandbox present, prod ET/ORCH absent | PASS |
|
||||
| C E2E | C7 Create issue in Plane SANDBOX | PASS |
|
||||
| C E2E | C8 Trigger pipeline via /webhook/plane | PASS |
|
||||
| C E2E | C9a Branch appears in orchestrator-sandbox | FAIL (waived — sandbox-infra) |
|
||||
| C E2E | C9b Analyst job enqueued in staging queue | FAIL (waived — sandbox-infra) |
|
||||
|
||||
CLEANUP completed: test Plane issue deleted (HTTP 204); no branch to delete.
|
||||
14
docs/work-items/ORCH-066/16-post-deploy-log.md
Normal file
14
docs/work-items/ORCH-066/16-post-deploy-log.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
post_deploy_status: HEALTHY
|
||||
action_taken: NONE
|
||||
work_item: ORCH-066
|
||||
window_s: 900
|
||||
checks_total: 30
|
||||
checks_failed: 0
|
||||
---
|
||||
|
||||
# Post-deploy log — ORCH-021 post-deploy monitor
|
||||
|
||||
Наблюдение прода завершено: `post_deploy_status: HEALTHY`, `action_taken: NONE`.
|
||||
|
||||
Окно наблюдения: 900s; опросов всего: 30, из них с провалом: 0.
|
||||
@@ -13,11 +13,25 @@
|
||||
# When set, the prevalidated (staging) image is retagged onto
|
||||
# TARGET_IMAGE instead of rebuilding — guarantees prod runs the
|
||||
# exact artefact that passed staging (no `docker build`).
|
||||
# EXPECTED_REVISION- expected git SHA of SOURCE_IMAGE (default: unset; ORCH-58)
|
||||
# Strategy B fail-closed provenance guard: when set, the
|
||||
# SOURCE_IMAGE's org.opencontainers.image.revision label MUST
|
||||
# equal this value before the BUILD-ONCE retag, else exit 1
|
||||
# (a stale image is never promoted). Unset -> no check (legacy).
|
||||
# GIT_SHA - build-arg for --build-staging (default: unset; ORCH-58)
|
||||
# BUILD_CONTEXT - docker build context dir (default: $REPO; --build-staging)
|
||||
# STAGING_CONTAINER- container to docker-exec staging_check in (--build-staging;
|
||||
# default: $TARGET_SERVICE → orchestrator-staging; ORCH-58)
|
||||
# STAGING_CHECK_PATH- staging_check.py path inside that container (--build-staging;
|
||||
# default: /repos/orchestrator/scripts/staging_check.py; ORCH-58)
|
||||
# STAGING_CHECK_MODE- staging_check mode stub|full-real (--build-staging;
|
||||
# default: stub — fast, no LLM spend; ORCH-58)
|
||||
# LOG - log file path (default: /var/log/orchestrator/deploy-hook.log)
|
||||
#
|
||||
# Usage:
|
||||
# ./orchestrator-deploy-hook.sh [--deploy] # normal deploy (default)
|
||||
# ./orchestrator-deploy-hook.sh --rollback # manual rollback
|
||||
# ./orchestrator-deploy-hook.sh [--deploy] # normal deploy (default)
|
||||
# ./orchestrator-deploy-hook.sh --rollback # manual rollback
|
||||
# ./orchestrator-deploy-hook.sh --build-staging # ORCH-58: rebuild staging image (8501)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -32,6 +46,11 @@ PREV_IMAGE_FILE="${PREV_IMAGE_FILE:-$REPO/.deploy-prev-image-staging}"
|
||||
# Build-once (ORCH-36): optional prevalidated source image to retag onto
|
||||
# TARGET_IMAGE. Unset -> backward-compatible (no retag), exit-code contract intact.
|
||||
SOURCE_IMAGE="${SOURCE_IMAGE:-}"
|
||||
# Provenance guard (ORCH-58, Strategy B): expected git SHA of SOURCE_IMAGE. Unset
|
||||
# -> backward-compatible (no provenance check), exit-code contract intact.
|
||||
EXPECTED_REVISION="${EXPECTED_REVISION:-}"
|
||||
# The OCI-standard label key the Dockerfile stamps with the build commit.
|
||||
REVISION_LABEL="org.opencontainers.image.revision"
|
||||
|
||||
# ---- Log setup -------------------------------------------------------------
|
||||
LOG_DIR=/var/log/orchestrator
|
||||
@@ -129,6 +148,57 @@ if [[ "${1:-}" == "--rollback" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# --build-staging mode (ORCH-58, Strategy A): rebuild the STAGING image from the
|
||||
# VALIDATED commit, recreate 8501, and run the AUTHORITATIVE staging_check against
|
||||
# the fresh image, so the artefact we validate is the exact one later BUILD-ONCE
|
||||
# retagged to prod (INV-FRESH, AC-4). Builds/recreates STAGING ONLY (8501) — never
|
||||
# prod (8500). Same exit-code contract (0 = healthy + staging_check PASS).
|
||||
# GIT_SHA - commit stamped into the image revision label (build-arg).
|
||||
# BUILD_CONTEXT - docker build context (host worktree of the validated commit).
|
||||
# Steps: (1) docker build → (2) recreate 8501 → (3a) health-check →
|
||||
# (3b) staging_check.py --mode stub against the fresh 8501 (ADR-001 step 3).
|
||||
# ============================================================================
|
||||
if [[ "${1:-}" == "--build-staging" ]]; then
|
||||
BUILD_CONTEXT="${BUILD_CONTEXT:-$REPO}"
|
||||
GIT_SHA="${GIT_SHA:-}"
|
||||
log "BUILD-STAGING: rebuilding $TARGET_IMAGE from $BUILD_CONTEXT (GIT_SHA=$GIT_SHA, port=$TARGET_PORT)"
|
||||
if ! docker build --build-arg GIT_SHA="$GIT_SHA" -t "$TARGET_IMAGE" "$BUILD_CONTEXT" >> "$LOG" 2>&1; then
|
||||
log "BUILD-STAGING: docker build failed - aborting (exit 1)"
|
||||
exit 1
|
||||
fi
|
||||
log "BUILD-STAGING: recreating $TARGET_SERVICE (profile=$COMPOSE_PROFILE) on the fresh image"
|
||||
if [[ -n "$COMPOSE_PROFILE" ]]; then
|
||||
docker compose --profile "$COMPOSE_PROFILE" up -d --no-build "$TARGET_SERVICE" >> "$LOG" 2>&1
|
||||
else
|
||||
docker compose up -d --no-build "$TARGET_SERVICE" >> "$LOG" 2>&1
|
||||
fi
|
||||
log "BUILD-STAGING: running health-check on port $TARGET_PORT (10x6s)"
|
||||
if ! health_check 10 6 "build-staging-health"; then
|
||||
log "BUILD-STAGING: health FAILED after rebuild (exit 1)"
|
||||
exit 1
|
||||
fi
|
||||
log "BUILD-STAGING: $TARGET_SERVICE healthy on fresh image"
|
||||
# (3b) ORCH-58 (Strategy A, step 3 — ADR-001): authoritative e2e validation of
|
||||
# the FRESH image. Run staging_check.py against the just-rebuilt 8501 INSIDE the
|
||||
# staging container (ORCH-048 canonical: it reads its OWN staging registry env, so
|
||||
# B6 is correct; the script lives at /repos/... via bind-mount, not in /app). This
|
||||
# is the same artefact later BUILD-ONCE retagged to prod, so we validate exactly
|
||||
# what we promote (AC-4). Any non-zero (FAIL or ORCH_STAGING safety-abort) -> exit 1
|
||||
# -> freshness gate FAIL -> rollback to development. Same exit-code contract.
|
||||
STAGING_CONTAINER="${STAGING_CONTAINER:-$TARGET_SERVICE}"
|
||||
STAGING_CHECK_PATH="${STAGING_CHECK_PATH:-/repos/orchestrator/scripts/staging_check.py}"
|
||||
STAGING_CHECK_MODE="${STAGING_CHECK_MODE:-stub}"
|
||||
log "BUILD-STAGING: running staging_check (--mode $STAGING_CHECK_MODE) against fresh http://localhost:$TARGET_PORT inside $STAGING_CONTAINER"
|
||||
if docker exec "$STAGING_CONTAINER" python3 "$STAGING_CHECK_PATH" \
|
||||
--base-url "http://localhost:$TARGET_PORT" --mode "$STAGING_CHECK_MODE" >> "$LOG" 2>&1; then
|
||||
log "BUILD-STAGING: staging_check PASS on fresh image (exit 0)"
|
||||
exit 0
|
||||
fi
|
||||
log "BUILD-STAGING: staging_check FAILED on fresh image - artefact not promotable (exit 1)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# NORMAL DEPLOY mode (--deploy or no argument)
|
||||
# ============================================================================
|
||||
@@ -156,6 +226,20 @@ git pull origin main >> "$LOG" 2>&1
|
||||
# Backward compatible: skipped when SOURCE_IMAGE is unset.
|
||||
if [[ -n "$SOURCE_IMAGE" ]]; then
|
||||
if docker image inspect "$SOURCE_IMAGE" >/dev/null 2>&1; then
|
||||
# ORCH-58 (Strategy B): fail-closed provenance guard BEFORE docker tag.
|
||||
# When EXPECTED_REVISION is set, SOURCE_IMAGE's git-commit label MUST match,
|
||||
# else exit 1 (FAILED -> БАГ-8 rollback); prod is NEVER touched. Empty label
|
||||
# / inspect error / mismatch all fail-close. Unset EXPECTED_REVISION -> no
|
||||
# check (backward-compatible for non-self repos / legacy calls).
|
||||
if [[ -n "$EXPECTED_REVISION" ]]; then
|
||||
IMG_REV=$(docker image inspect --format "{{ index .Config.Labels \"$REVISION_LABEL\" }}" "$SOURCE_IMAGE" 2>/dev/null || true)
|
||||
if [[ "$IMG_REV" == "<no value>" ]]; then IMG_REV=""; fi
|
||||
if [[ -z "$IMG_REV" || "$IMG_REV" != "$EXPECTED_REVISION" ]]; then
|
||||
log "PROVENANCE: SOURCE_IMAGE revision '$IMG_REV' != expected '$EXPECTED_REVISION' (fail-closed) - aborting (exit 1)"
|
||||
exit 1
|
||||
fi
|
||||
log "PROVENANCE: SOURCE_IMAGE revision matches expected ($EXPECTED_REVISION) - retag allowed"
|
||||
fi
|
||||
log "BUILD-ONCE: retagging $SOURCE_IMAGE -> $TARGET_IMAGE (no rebuild)"
|
||||
docker tag "$SOURCE_IMAGE" "$TARGET_IMAGE" >> "$LOG" 2>&1
|
||||
else
|
||||
|
||||
@@ -51,6 +51,46 @@ import datetime
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
from collections import namedtuple
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ORCH-061: pure staging-verdict logic (classification + infra-tolerant verdict).
|
||||
# Imported from src.staging_verdict — a stdlib-only leaf, safe to import inside
|
||||
# the orchestrator-staging container (PYTHONPATH=/app, pattern B6 / ORCH-048).
|
||||
# Guarded so the suite still runs (in strict mode) if src is somehow unimportable
|
||||
# from a host invocation; the fallback NEVER yields a silent green (fail-closed).
|
||||
# ---------------------------------------------------------------------------
|
||||
try:
|
||||
from src.staging_verdict import ( # type: ignore
|
||||
classify_check as _classify_check,
|
||||
compute_staging_verdict as _compute_staging_verdict,
|
||||
REAL as _REAL,
|
||||
SANDBOX_INFRA as _SANDBOX_INFRA,
|
||||
)
|
||||
except Exception: # pragma: no cover - exercised only on a broken host import
|
||||
_classify_check = None
|
||||
_compute_staging_verdict = None
|
||||
_REAL = "real"
|
||||
_SANDBOX_INFRA = "sandbox_infra"
|
||||
|
||||
_FallbackVerdict = namedtuple("StagingVerdict", "status exit_code waived summary")
|
||||
|
||||
|
||||
def _classify(label: str) -> str:
|
||||
"""Classify a check label via staging_verdict; fail-closed to REAL if absent."""
|
||||
if _classify_check is not None:
|
||||
return _classify_check(label)
|
||||
return _REAL
|
||||
|
||||
|
||||
def _verdict(items, infra_tolerant: bool):
|
||||
"""Compute the suite verdict via staging_verdict; strict fail-closed fallback."""
|
||||
if _compute_staging_verdict is not None:
|
||||
return _compute_staging_verdict(items, infra_tolerant)
|
||||
failed = [lbl for (lbl, ok, _cat) in items if not ok]
|
||||
if failed:
|
||||
return _FallbackVerdict("FAILED", 1, [], f"FAILED (strict fallback): {failed}")
|
||||
return _FallbackVerdict("SUCCESS", 0, [], "SUCCESS (strict fallback): all green")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Colour helpers
|
||||
@@ -152,23 +192,47 @@ def _sign_payload(secret: str, body: bytes) -> str:
|
||||
|
||||
class Results:
|
||||
def __init__(self):
|
||||
# _items keeps the (label, passed, detail) 3-tuple shape that existing
|
||||
# ORCH-048 B6 tests unpack — categories live in a PARALLEL list so the
|
||||
# public tuple contract is unchanged.
|
||||
self._items: list[tuple[str, bool, str]] = [] # (label, passed, detail)
|
||||
self._categories: list[str] = [] # ORCH-061: REAL | SANDBOX_INFRA
|
||||
|
||||
def add(self, label: str, passed: bool, detail: str = ""):
|
||||
def add(self, label: str, passed: bool, detail: str = "", category: str | None = None):
|
||||
# ORCH-061: every check carries a category. None -> auto-classify by label
|
||||
# (C9a/C9b -> SANDBOX_INFRA, everything else -> REAL). Fail-closed: an
|
||||
# unknown label is REAL, so it still counts toward the safety net.
|
||||
if category is None:
|
||||
category = _classify(label)
|
||||
self._items.append((label, passed, detail))
|
||||
self._categories.append(category)
|
||||
line = _ok(label) if passed else _fail(label)
|
||||
if detail:
|
||||
line += f" [{detail}]"
|
||||
print(line)
|
||||
|
||||
def categorized_items(self) -> list[tuple[str, bool, str]]:
|
||||
"""Rows as ``(label, passed, category)`` for ``compute_staging_verdict``."""
|
||||
return [
|
||||
(label, passed, cat)
|
||||
for (label, passed, _detail), cat in zip(self._items, self._categories)
|
||||
]
|
||||
|
||||
def summary(self) -> bool:
|
||||
passed = sum(1 for _, ok, _ in self._items if ok)
|
||||
total = len(self._items)
|
||||
all_ok = passed == total
|
||||
colour = _GREEN if all_ok else _RED
|
||||
# ORCH-061: per-category breakdown so an operator can tell a REAL failure
|
||||
# (regression — fail-closed) from a SANDBOX_INFRA one (waivable).
|
||||
rows = self.categorized_items()
|
||||
real_fail = [lbl for lbl, ok, cat in rows if not ok and cat == _REAL]
|
||||
infra_fail = [lbl for lbl, ok, cat in rows if not ok and cat == _SANDBOX_INFRA]
|
||||
print()
|
||||
print(f"{_BOLD}{'='*60}{_RESET}")
|
||||
print(f"{colour}{_BOLD} RESULT: {passed}/{total} checks PASS{_RESET}")
|
||||
print(f" REAL failed : {real_fail or 'none'}")
|
||||
print(f" SANDBOX_INFRA failed: {infra_fail or 'none'}")
|
||||
print(f"{_BOLD}{'='*60}{_RESET}")
|
||||
return all_ok
|
||||
|
||||
@@ -637,6 +701,28 @@ def _cleanup(plane_base, workspace, gitea_base, plane_headers, gitea_headers,
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _resolve_tolerance(cli_strict: bool) -> bool:
|
||||
"""Resolve whether the infra-FAIL waiver is active (ORCH-061).
|
||||
|
||||
Precedence: an explicit ``--strict`` CLI flag forces it OFF (for honest manual
|
||||
runs). Otherwise read ``settings.staging_infra_tolerance_enabled`` from the
|
||||
running instance's own config (same pattern as B6's src.* import inside the
|
||||
container). On ANY import/read error -> STRICT (False): we never waive when the
|
||||
config is unreadable (fail-safe), and we say so.
|
||||
"""
|
||||
if cli_strict:
|
||||
print(_info("tolerance: DISABLED via --strict (honest run)"))
|
||||
return False
|
||||
try:
|
||||
from src.config import settings # noqa: WPS433 - lazy, mirrors B6
|
||||
enabled = bool(settings.staging_infra_tolerance_enabled)
|
||||
print(_info(f"tolerance: staging_infra_tolerance_enabled={enabled}"))
|
||||
return enabled
|
||||
except Exception as e:
|
||||
print(_info(f"tolerance: config unavailable, defaulting to STRICT: {e}"))
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Live staging-stand check suite (ORCH-33)"
|
||||
@@ -656,6 +742,15 @@ def main():
|
||||
"full-real: also wait for the analyst agent (slow, costs credits)."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help=(
|
||||
"ORCH-061: force strict suite — disable the sandbox-infra (C9a/C9b) "
|
||||
"FAIL waiver even if staging_infra_tolerance_enabled=True. Use for an "
|
||||
"honest 10/10 run once the sandbox bot accounts are provisioned."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
base = args.base_url.rstrip("/")
|
||||
@@ -673,8 +768,23 @@ def main():
|
||||
block_b(results)
|
||||
block_c(base, results, args.mode)
|
||||
|
||||
all_ok = results.summary()
|
||||
sys.exit(0 if all_ok else 1)
|
||||
results.summary()
|
||||
|
||||
# ORCH-061: the EXIT CODE (which drives the deployer's staging_status verdict)
|
||||
# comes from the infra-tolerant verdict, NOT a raw passed==total count. A run
|
||||
# whose only failures are known sandbox-infra checks (C9a/C9b) is waived to
|
||||
# exit 0 when tolerance is on; ANY real check failure still exits 1 (FR-4).
|
||||
infra_tolerant = _resolve_tolerance(args.strict)
|
||||
verdict = _verdict(results.categorized_items(), infra_tolerant)
|
||||
if verdict.waived:
|
||||
# FR-7 observability: make "green with an allowance" distinguishable from
|
||||
# an honest green in the logs / captured deployer output.
|
||||
print(f"{_YELLOW}{_BOLD}INFRA-WAIVED:{_RESET} "
|
||||
f"{', '.join(verdict.waived)} "
|
||||
f"(known sandbox-infra; real checks green)")
|
||||
print(f"{_BOLD}VERDICT:{_RESET} {verdict.status} "
|
||||
f"(exit {verdict.exit_code}) — {verdict.summary}")
|
||||
sys.exit(verdict.exit_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -20,6 +20,33 @@ logger = logging.getLogger("orchestrator.launcher")
|
||||
# never passed through to the CLI.
|
||||
VALID_EFFORTS = frozenset({"low", "medium", "high", "xhigh", "max"})
|
||||
|
||||
# ORCH-061: action stages whose success is an ACTION (restart/retag), not a src
|
||||
# edit — so "no changes to commit" is EXPECTED there, not under-delivery (FR-3).
|
||||
_ACTION_STAGES = frozenset({"deploy-staging", "deploy"})
|
||||
|
||||
|
||||
def action_stage_no_changes_note(stage, repo) -> str | None:
|
||||
"""ORCH-061 (FR-3 / FR-7): observability for an empty diff on an action stage.
|
||||
|
||||
The ``deploy-staging`` / ``deploy`` stages are actions (restart / retag), not
|
||||
code edits, so the post-run "no changes to commit" is the NORMAL case there —
|
||||
advancement is decided by the agent exit-code + the staging/deploy gate verdict,
|
||||
NEVER by the presence of a commit (FR-3 / AC-4). This is a PURE decision used
|
||||
only to emit an explicit log line distinguishing an expected action-stage no-op
|
||||
from a code-stage no-op; it has no effect on stage advancement.
|
||||
|
||||
Returns an explicit note string when the empty diff is expected (an action
|
||||
stage of a self-deploy repo), else ``None``. Never raises.
|
||||
"""
|
||||
try:
|
||||
if stage in _ACTION_STAGES:
|
||||
from ..self_deploy import self_deploy_applies
|
||||
if self_deploy_applies(repo):
|
||||
return f"{stage}: no code changes (expected on action stage)"
|
||||
return None
|
||||
except Exception: # noqa: BLE001 - observability only, never raise
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_agent_attr(agent, project_id, project_map_attr, env_attr_prefix,
|
||||
default_attr):
|
||||
@@ -222,6 +249,11 @@ class AgentLauncher:
|
||||
"""
|
||||
if job.get("agent") == "deploy-finalizer":
|
||||
return self._run_deploy_finalizer_job(job)
|
||||
# ORCH-021: the reserved-agent `post-deploy-monitor` is also a
|
||||
# DETERMINISTIC (no-LLM) tick — intercept it BEFORE _spawn and run one
|
||||
# observation tick synchronously. Returns None (no agent_run row).
|
||||
if job.get("agent") == "post-deploy-monitor":
|
||||
return self._run_post_deploy_monitor_job(job)
|
||||
return self._spawn(
|
||||
job["agent"],
|
||||
job["repo"],
|
||||
@@ -251,6 +283,27 @@ class AgentLauncher:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _run_post_deploy_monitor_job(self, job: dict):
|
||||
"""ORCH-021: run one deterministic post-deploy monitor tick for a job.
|
||||
|
||||
Not an LLM spawn — there is no subprocess/monitor, so we mark the jobs row
|
||||
done/failed here. The tick never-raises, but we guard anyway so a monitor
|
||||
fault can never wedge the worker / starve other projects (AC-16).
|
||||
"""
|
||||
from ..db import mark_job
|
||||
from .. import stage_engine
|
||||
try:
|
||||
stage_engine.run_post_deploy_monitor(job)
|
||||
mark_job(job["id"], "done")
|
||||
logger.info(f"post-deploy-monitor job {job['id']} done")
|
||||
except Exception as e:
|
||||
logger.error(f"post-deploy-monitor job {job['id']} failed: {e}")
|
||||
try:
|
||||
mark_job(job["id"], "failed", error=f"post-deploy-monitor error: {e}")
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _spawn(self, agent: str, repo: str, task_content: str = None,
|
||||
task_id: int = None, job_id: int = None) -> int:
|
||||
"""Shared spawn implementation for launch() and launch_job().
|
||||
@@ -364,6 +417,14 @@ class AgentLauncher:
|
||||
"UPDATE agent_runs SET output_path = ? WHERE id = ?",
|
||||
(output_path, run_id),
|
||||
)
|
||||
# ORCH-065: stamp the agent process pid onto the job row so the job-reaper
|
||||
# can probe liveness (os.kill(pid, 0)). proc.pid only exists after Popen,
|
||||
# so this is a second UPDATE next to run_id/started_at (set above in _spawn).
|
||||
if job_id is not None:
|
||||
conn.execute(
|
||||
"UPDATE jobs SET pid = ? WHERE id = ?",
|
||||
(proc.pid, job_id),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -582,6 +643,22 @@ class AgentLauncher:
|
||||
logger.warning(f"Agent run_id={run_id}: commit failed: {commit_result.stderr}")
|
||||
else:
|
||||
logger.info(f"Agent run_id={run_id}: no changes to commit")
|
||||
# ORCH-061: on a self-deploy action stage (deploy-staging/deploy)
|
||||
# an empty diff is EXPECTED (action, not a src edit). Emit an
|
||||
# explicit observability line so an operator can tell this apart
|
||||
# from a code-stage no-op. Does NOT affect advancement (decided by
|
||||
# exit-code + gate verdict, never by a commit existing).
|
||||
try:
|
||||
_t = get_task_by_repo_branch(repo, branch)
|
||||
_stage = _t["stage"] if _t else None
|
||||
_note = action_stage_no_changes_note(_stage, repo)
|
||||
if _note:
|
||||
logger.info(f"Agent run_id={run_id}: {_note}")
|
||||
except Exception as _e:
|
||||
logger.debug(
|
||||
f"Agent run_id={run_id}: action-stage no-changes note "
|
||||
f"skipped: {_e}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Agent run_id={run_id}: post-run git failed: {e}")
|
||||
|
||||
|
||||
115
src/config.py
115
src/config.py
@@ -195,6 +195,46 @@ class Settings(BaseSettings):
|
||||
deploy_prod_target_image: str = "orchestrator-orchestrator"
|
||||
deploy_prod_compose_profile: str = ""
|
||||
deploy_prod_prev_image_file: str = ".deploy-prev-image-prod"
|
||||
|
||||
# ORCH-058: staging-image provenance before the BUILD-ONCE retag to prod.
|
||||
# Closes the INV-FRESH gap (ADR-001): the BUILD-ONCE retag (ORCH-36) promotes
|
||||
# the staging image to prod WITHOUT a rebuild, assuming the staging image is
|
||||
# fresh — a guarantee the pipeline never had (a stale image could be silently
|
||||
# promoted, LESSONS_ORCH-036 §4). Two complementary layers, self-hosting only:
|
||||
# A (liveness): the QG sub-check check_staging_image_fresh rebuilds the
|
||||
# staging image from the VALIDATED commit (worktree HEAD after merge-gate)
|
||||
# and recreates 8501 on the deploy-staging -> deploy edge, so we validate
|
||||
# and promote ONE artefact.
|
||||
# B (safety): build_deploy_command passes EXPECTED_REVISION and the hook
|
||||
# fail-closes (exit 1) if SOURCE_IMAGE's revision label != EXPECTED_REVISION
|
||||
# before `docker tag`, making a silent stale promote structurally impossible.
|
||||
#
|
||||
# image_freshness_enabled -> SINGLE kill-switch for the WHOLE feature (A + B
|
||||
# together; never "B without A" = a deadlock). False
|
||||
# -> legacy ORCH-36 behaviour (BUILD-ONCE, no guard,
|
||||
# no EXPECTED_REVISION). Env ORCH_IMAGE_FRESHNESS_ENABLED.
|
||||
# image_freshness_repos -> CSV of repos where the feature is REAL; empty ->
|
||||
# only the self-hosting repo (orchestrator). Mirrors
|
||||
# self_deploy_repos / merge_gate_repos.
|
||||
image_freshness_enabled: bool = True
|
||||
image_freshness_repos: str = ""
|
||||
|
||||
# ORCH-061: tolerate KNOWN sandbox-infra FAILs (C9a/C9b) in the staging suite.
|
||||
# The self-hosting deploy-staging stage looped because scripts/staging_check.py
|
||||
# exited non-zero on ANY failed check, so two infra-only failures (sandbox bot
|
||||
# accounts not members of the sandbox Plane project) produced staging_status:
|
||||
# FAILED -> rollback deploy-staging -> development -> loop.
|
||||
# True -> a run whose ONLY failures are allowlisted sandbox-infra checks
|
||||
# (C9a/C9b) is waived to SUCCESS; ANY real pipeline check that fails
|
||||
# still fails closed -> FAILED -> rollback (safety net intact, FR-4).
|
||||
# False -> 1:1 pre-ORCH-061 strict behaviour: any FAIL -> FAILED -> rollback.
|
||||
# Default True (mirrors merge_gate_enabled / image_freshness_enabled /
|
||||
# self_deploy_enabled): the safety net holds regardless of the flag; the flag
|
||||
# exists to instantly restore legacy strictness without a code redeploy. Lives
|
||||
# in .env.staging (ORCH_ prefix) so it is reachable inside orchestrator-staging.
|
||||
# Env ORCH_STAGING_INFRA_TOLERANCE_ENABLED.
|
||||
staging_infra_tolerance_enabled: bool = True
|
||||
|
||||
# ORCH-053: stuck-task reconciler (sweeper for lost webhooks). A background
|
||||
# daemon thread reconciles the "source of truth (gate / Plane) != task stage"
|
||||
# drift left behind by a dropped webhook (502 on rebuild, no Plane/Gitea
|
||||
@@ -210,12 +250,87 @@ class Settings(BaseSettings):
|
||||
# JSON -> default (mirrors agent_timeout_overrides_json).
|
||||
# reconcile_notify_unblock -> send a Telegram message when a stuck task is
|
||||
# unblocked (F-4 observability).
|
||||
# reconcile_skip_blocked_enabled -> ORCH-060 Guard 2: skip F-1 reconciliation of
|
||||
# issues a human moved to Blocked / Needs Input
|
||||
# (per-candidate Plane state lookup). Disabling it
|
||||
# mutes ONLY the networked Guard 2; Guard 1
|
||||
# (escalated-by-retries, local + deterministic) is
|
||||
# always active. Manual escape hatch during a Plane
|
||||
# outage.
|
||||
reconcile_enabled: bool = True
|
||||
reconcile_interval_s: int = 120
|
||||
reconcile_plane_enabled: bool = True
|
||||
reconcile_grace_default_s: int = 600
|
||||
reconcile_grace_overrides_json: str = ""
|
||||
reconcile_notify_unblock: bool = True
|
||||
reconcile_skip_blocked_enabled: bool = True
|
||||
|
||||
# ORCH-021: post-deploy production monitoring + degradation reaction. After
|
||||
# the terminal deploy->done transition for an applicable repo, a reserved-agent
|
||||
# `post-deploy-monitor` job (no LLM, modelled on deploy-finalizer) probes prod
|
||||
# over a window and reacts to a degradation the restart-time health-check
|
||||
# missed (class "green deploy, red prod", precedent ET-8). State is in sentinel
|
||||
# files (.post-deploy-state-<repo>/<wi>/), no DB migration. See
|
||||
# docs/architecture/adr/adr-0010-post-deploy-monitor.md.
|
||||
# post_deploy_monitor_enabled -> global kill-switch (BR-8); False -> the
|
||||
# pipeline is 1:1 as before ORCH-021 (no arm).
|
||||
# post_deploy_repos -> CSV of repos where monitoring is REAL; empty
|
||||
# -> only the self-hosting repo (orchestrator).
|
||||
# Mirrors self_deploy_repos / merge_gate_repos.
|
||||
# post_deploy_window_s -> observation window length (~15 min, BR-1).
|
||||
# post_deploy_interval_s -> seconds between probe ticks.
|
||||
# post_deploy_fail_threshold -> N CONSECUTIVE health failures -> DEGRADED.
|
||||
# post_deploy_5xx_threshold -> window 5xx ratio above this -> DEGRADED.
|
||||
# post_deploy_auto_rollback -> globally allow auto-rollback; True acts ONLY
|
||||
# for non-self repos. For self-hosting the
|
||||
# reaction is ALWAYS ALERT_ONLY (BR-5) — a tick
|
||||
# NEVER restarts the prod orchestrator container.
|
||||
# post_deploy_base_url -> base URL of the observed prod instance.
|
||||
# Rollback target params reuse the existing deploy_prod_* settings (no dupes).
|
||||
post_deploy_monitor_enabled: bool = True
|
||||
post_deploy_repos: str = ""
|
||||
post_deploy_window_s: int = 900
|
||||
post_deploy_interval_s: int = 30
|
||||
post_deploy_fail_threshold: int = 3
|
||||
post_deploy_5xx_threshold: float = 0.5
|
||||
post_deploy_auto_rollback: bool = False
|
||||
post_deploy_base_url: str = "http://localhost:8500"
|
||||
|
||||
# ORCH-065: job-reaper + proactive merge-lease reclaim. A background daemon
|
||||
# thread (modelled on the reconciler) makes "the monitor thread / process died
|
||||
# while a job/lease was held" self-heal WITHOUT a restart. Status (done/queued/
|
||||
# failed) is otherwise only ever set by launcher._monitor_agent -> _finalize_job
|
||||
# inside the live process; a death there left the jobs row 'running' forever and
|
||||
# (at max_concurrency=1) wedged the queue of EVERY project (incidents 07.06: jobs
|
||||
# 236/239/242/254). The same thread proactively reclaims a stale/dead merge-lease
|
||||
# (ORCH-043) instead of waiting for the lazy TTL on the next foreign acquire. See
|
||||
# docs/architecture/adr/adr-0011-job-reaper-lease-reclaim.md.
|
||||
# reaper_enabled -> global kill-switch (false -> strictly prior behaviour;
|
||||
# only the startup requeue_running_jobs remains).
|
||||
# reaper_interval_s -> background scan period (seconds).
|
||||
# reaper_dead_ticks -> Tier-1: consecutive ticks a job's pid must be dead
|
||||
# before it is reaped (>=2 anti-false-positive; a live
|
||||
# long-running agent is NEVER reaped).
|
||||
# reaper_max_running_s -> Tier-3 backstop ceiling: a job 'running' longer than
|
||||
# this is reaped even when liveness is unknowable. MUST be
|
||||
# > max agent_timeout + grace so a legit agent is safe.
|
||||
# reaper_finalize_grace_s -> Tier-2 anti-false-positive: a LIVE monitor writes
|
||||
# agent_runs.exit_code FIRST, THEN does git commit/push +
|
||||
# PR + Plane usage comments (seconds..minutes) and only
|
||||
# then _finalize_job. The agent pid is already dead in
|
||||
# that window, so pid cannot tell "monitor died" from
|
||||
# "monitor still finalizing". A job is reaped via Tier-2
|
||||
# only once exit_code has been recorded for at least this
|
||||
# many seconds (MUST be > the max finalization window).
|
||||
# lease_reclaim_enabled -> kill-switch for the proactive stale/dead lease reclaim
|
||||
# (false -> only the legacy lazy TTL reclaim in acquire).
|
||||
# (reuse) merge_lock_timeout_s -> lease TTL; merge_gate_repos -> reclaim scope.
|
||||
reaper_enabled: bool = True
|
||||
reaper_interval_s: int = 60
|
||||
reaper_dead_ticks: int = 2
|
||||
reaper_max_running_s: int = 3600
|
||||
reaper_finalize_grace_s: int = 300
|
||||
lease_reclaim_enabled: bool = True
|
||||
|
||||
# Telegram notifications
|
||||
telegram_bot_token: str = ""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user