Compare commits
294 Commits
staging-lo
...
dd4aaebe84
| Author | SHA1 | Date | |
|---|---|---|---|
| dd4aaebe84 | |||
| f645090e4d | |||
| ee4773f5b0 | |||
| 4597a8471d | |||
| b478b38df5 | |||
| 99cafefba6 | |||
| 85cfce451f | |||
| a23d4c0971 | |||
|
|
49fad5e458 | ||
| d9bb8d5fe3 | |||
| 32cc965f84 | |||
| 81fc2df8a8 | |||
| a7b27f2235 | |||
| 36c7a68722 | |||
| 18fb2eb17d | |||
| c86dc3ca95 | |||
| 77714aa318 | |||
| 493b9be9c4 | |||
|
|
1b095282bf | ||
| 9c19588bcd | |||
| fe3f1658ba | |||
| 595c382ac7 | |||
| aa488edddf | |||
| f2161451a0 | |||
| 0e7d608fc0 | |||
| fb9390e216 | |||
| 92817889c4 | |||
|
|
baf7860822 | ||
| 2cf40c1af9 | |||
| 44ef0bb570 | |||
| d826eacfcf | |||
| a482b36dae | |||
| f452626bb8 | |||
| b46fc6e51b | |||
| 140827f4da | |||
| fc29ba76ec | |||
|
|
9834dae108 | ||
| 039322001a | |||
| 1997376eb5 | |||
| 0ab6a33ef5 | |||
| 74269b467c | |||
| 781f9df26c | |||
| c0715ad55b | |||
| 7ee528ad7b | |||
| 2861dea613 | |||
| 50434fc2b1 | |||
|
|
6eb9992585 | ||
| e9b23d3c04 | |||
| e3c3292ec7 | |||
| 1ada41f272 | |||
| 62b4d1f7d1 | |||
| c5007e6c90 | |||
| 10510ac48c | |||
| 8ccd17e199 | |||
| 30d9effea1 | |||
| a091a2d999 | |||
|
|
b371b6d940 | ||
| ea094f5922 | |||
| 17258fb69e | |||
| 0873803faa | |||
| 0c240198e4 | |||
| 1e1811a4bc | |||
| e89f7c7a11 | |||
| 0f82ebc1a7 | |||
| d04be97c0e | |||
| b0e517c76a | |||
|
|
662d2d6434 | ||
|
|
90a5cae8e6 | ||
|
|
1d928dab57 | ||
| 9800dc89e3 | |||
| 5b80f8facb | |||
| a74379f657 | |||
| 9019e12d98 | |||
| 518d7d18c8 | |||
| 520bcafa73 | |||
| 9f7b6edb6d | |||
| 1c3ecb973e | |||
|
|
1b45fa0008 | ||
| 1f0929838a | |||
| 7deb151ce5 | |||
| aff334e82b | |||
| fa9b96545c | |||
| 319b23b4fc | |||
| e54d1fc4ac | |||
| 77abfb399c | |||
| 05bd169b14 | |||
|
|
183e6d68bc | ||
|
|
befa2979ec | ||
|
|
d33e0ded2e | ||
| de70ee811d | |||
|
|
41da03470a | ||
| e1055861b5 | |||
| 2e84813c13 | |||
| 18f887c886 | |||
| 37ef58f21f | |||
| 0b9ae514c9 | |||
| c56672aabf | |||
| 0ed05417e6 | |||
| 7d99782673 | |||
| 59603f6e92 | |||
| d5f11e5caa | |||
| affbb259a1 | |||
| 8149eb7769 | |||
|
|
9979eec168 | ||
| c991b9de1a | |||
| 3d7d751b7a | |||
| f330a580c4 | |||
| 896ecf6acb | |||
| 096c452230 | |||
| 9f176036f1 | |||
| 3e4191050f | |||
| 38e329f6f7 | |||
| 58d6c433d1 | |||
| 52ca882e5b | |||
| d49e88cf3f | |||
| e7a5b50f97 | |||
| 034343ec5d | |||
| cc87beb2b4 | |||
| fb25e9a0cf | |||
| 2824fd8543 | |||
| c26a6b637c | |||
| dd5fe619d5 | |||
| f6b5671267 | |||
| 49461238f1 | |||
|
|
c90c01b919 | ||
|
|
2ec6873e33 | ||
|
|
cac6539698 | ||
|
|
af7472df05 | ||
|
|
995ba0af71 | ||
| 772ccab013 | |||
|
|
06271b0bfb | ||
| 101bd1c512 | |||
|
|
aa4161fc78 | ||
| 6bbd530caa | |||
| 4b03f213f7 | |||
| 1d72c44587 | |||
| 0605309602 | |||
| 044894cbe9 | |||
| cb11137a77 | |||
| 48b54051e5 | |||
|
|
72d662ae88 | ||
|
|
348cf8c164 | ||
| bc2347abd3 | |||
| 62c1fe3461 | |||
| 0dfddf93f0 | |||
| 22d3b77426 | |||
| 4a06537afd | |||
| b6c0e11e4d | |||
| 3fb3d15cb4 | |||
|
|
9f4d79baee | ||
|
|
7cdef6d377 | ||
|
|
0cbb7ef0bb | ||
| ca41d9210b | |||
| 48943fe10a | |||
| 86fe8dd509 | |||
| dd07b58165 | |||
| b67a61ecef | |||
| 8fcb867dcf | |||
| 4815e378d9 | |||
|
|
e07ee9e574 | ||
| 8cdb9f194a | |||
| cb3bdd9c7a | |||
|
|
04233cb3c8 | ||
|
|
85ecf50926 | ||
| 30b6187c73 | |||
| 44db94e462 | |||
| 4f24f96169 | |||
| 2d20da295e | |||
| 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 | |||
|
|
36c1898fac | ||
| e2dc9d6df6 | |||
| c0bcb544cf | |||
| 2be39b398b | |||
| d79defeadd | |||
| 9f43e6a0ae | |||
| 10f2a39a58 | |||
| 63187ff102 | |||
| 5c5525548d | |||
| 0d0cd6e281 | |||
| 480b203a9d | |||
| 7705552f08 | |||
| c1196e34e8 | |||
| d43603b224 | |||
| 682ae09316 | |||
| 5089f99bb1 | |||
| 32161a180a | |||
| 7d2d77217a | |||
| f5aae50514 | |||
| a083ed8495 | |||
| eac0eb4b3a | |||
| 434bd6243d | |||
| c21a279565 | |||
| d9afb3a10d | |||
| 8447853db8 | |||
| 5dc5893a49 | |||
| 581a8b595a | |||
| ba51aa17bc | |||
| 00d69d9e27 | |||
| ad1589084b | |||
| 77e7205ce8 | |||
| 445807dd90 | |||
| 39cb5dde70 | |||
| 7b748b7ac5 | |||
| bcf5256731 | |||
| 80275a3336 | |||
| 59e47ba067 | |||
| be64761654 | |||
| f81715bd39 | |||
| fe5eb38af2 | |||
| 5436c4110e | |||
| 8e91c8c23c | |||
| 83e26279bf | |||
| 3441f01650 | |||
| 18378c2713 | |||
| 753eea37fc | |||
| efbd8b7b8f | |||
| 6ef28efccd | |||
| 52cfe51bd8 | |||
| 05c17135c1 | |||
| 0ac50b8c73 | |||
| 66100855f6 | |||
| 3f23897327 | |||
| ed10f28879 | |||
| 45480966c1 |
306
.env.example
306
.env.example
@@ -12,3 +12,309 @@ ORCH_GITEA_WEBHOOK_SECRET=
|
||||
ORCH_CLAUDE_BIN=/usr/bin/claude
|
||||
ORCH_REPOS_DIR=/home/slin/repos
|
||||
ORCH_DB_PATH=/app/data/orchestrator.db
|
||||
|
||||
# ── Agent model / effort / fallback (ORCH-41, validation ORCH-74) ─────────────
|
||||
# Per-agent LLM model + reasoning effort, resolved by launcher.resolve_agent_*.
|
||||
# Resolution priority (per agent): project-override (projects_json agent_models/
|
||||
# agent_efforts) > ORCH_AGENT_MODEL_<AGENT> / ORCH_AGENT_EFFORT_<AGENT> >
|
||||
# ORCH_AGENT_MODEL_DEFAULT / ORCH_AGENT_EFFORT_DEFAULT > CLI default (no flag).
|
||||
# The frontmatter `model:` in .openclaw/agents/*.md is DESCRIPTIVE only and is NOT
|
||||
# read — config below is the single source of truth for the model (ORCH-74 G1).
|
||||
#
|
||||
# ORCH-74 (G2): a resolved MODEL name is validated (^claude-…$ format check) before
|
||||
# it reaches --model. A structurally invalid name (typo, gpt-4, empty) is logged and
|
||||
# the next valid level is used (in the limit: no --model flag). Forward-compatible:
|
||||
# a future claude-* version passes without editing any allowlist. EFFORT is validated
|
||||
# against low|medium|high|xhigh|max (ORCH-41); an invalid effort is dropped.
|
||||
#
|
||||
# All 6 agents resolve to claude-opus-4-8 (model-routing G3 NOT enabled). Leave the
|
||||
# per-agent overrides empty to use the default. Do NOT hardcode the model version
|
||||
# anywhere except ORCH_AGENT_MODEL_DEFAULT.
|
||||
ORCH_AGENT_MODEL_DEFAULT=claude-opus-4-8
|
||||
ORCH_AGENT_MODEL_ANALYST=
|
||||
ORCH_AGENT_MODEL_ARCHITECT=
|
||||
ORCH_AGENT_MODEL_DEVELOPER=
|
||||
ORCH_AGENT_MODEL_REVIEWER=
|
||||
ORCH_AGENT_MODEL_TESTER=
|
||||
ORCH_AGENT_MODEL_DEPLOYER=
|
||||
# Effort split (ORCH-081/ORCH-52h): thinking agents (analyst/architect/reviewer)
|
||||
# -> high; developer -> xhigh (coding/agentic role, Opus 4.8 canon); mechanical
|
||||
# agents (tester/deployer) -> medium. NB: an empty ORCH_AGENT_EFFORT_*= no longer
|
||||
# zeroes the effort — the launcher falls back to a per-role floor (= the config.py
|
||||
# class-default) so each role still runs at its canonical level (ORCH-081).
|
||||
ORCH_AGENT_EFFORT_DEFAULT=high
|
||||
ORCH_AGENT_EFFORT_ANALYST=high
|
||||
ORCH_AGENT_EFFORT_ARCHITECT=high
|
||||
ORCH_AGENT_EFFORT_DEVELOPER=xhigh
|
||||
ORCH_AGENT_EFFORT_REVIEWER=high
|
||||
ORCH_AGENT_EFFORT_TESTER=medium
|
||||
ORCH_AGENT_EFFORT_DEPLOYER=medium
|
||||
# Optional --fallback-model used when the primary is overloaded. Empty -> no flag
|
||||
# (G4 NOT enabled, ADR-001 ORCH-74: determinism — all agents stay on opus-4-8). A
|
||||
# non-empty value is validated by the SAME predicate as the model; a typo is dropped.
|
||||
ORCH_AGENT_FALLBACK_MODEL=
|
||||
# ORCH-042/ORCH-067: live-tracker mode. bump (DEFAULT since ORCH-067) -> on every
|
||||
# update the old card is deleted and a fresh one is sent silently to the BOTTOM of
|
||||
# the chat (deleteMessage + sendMessage + repoint), so the current status is always
|
||||
# the last message in an active chat. edit -> the task card is edited in place
|
||||
# (editMessageText). One card per task in both modes. Any value other than "bump"
|
||||
# (incl. empty/garbage) -> edit.
|
||||
ORCH_TRACKER_MODE=bump
|
||||
# ORCH-067: best-effort live-overlay for the card status line. The offline core
|
||||
# (stage -> Plane status, In Review from the brd-clock) always works without network;
|
||||
# the overlay only fills in branches indistinguishable offline (Needs Input / Blocked /
|
||||
# Rejected / Cancelled / Deploying / Monitoring after Deploy) by reading the LIVE Plane
|
||||
# status with a short timeout + per-issue TTL cache. It NEVER blocks the pipeline and
|
||||
# NEVER raises.
|
||||
# LIVE_STATUS -> kill-switch (false -> offline core only).
|
||||
# LIVE_STATUS_TTL_S -> TTL (seconds) of the per-issue live-uuid cache (hot-path guard).
|
||||
# LIVE_STATUS_TIMEOUT_S -> timeout (seconds) of a single live-GET on the render path.
|
||||
ORCH_TRACKER_LIVE_STATUS=true
|
||||
ORCH_TRACKER_LIVE_STATUS_TTL_S=60
|
||||
ORCH_TRACKER_LIVE_STATUS_TIMEOUT_S=3
|
||||
# ORCH-043: merge-gate (auto-rebase onto current origin/main + re-test + merge-lock)
|
||||
# on the deploy-staging -> deploy edge. Deterministic sub-gate (no LLM) that catches
|
||||
# the branch up to the CURRENT origin/main, re-tests it, and serialises merges so two
|
||||
# green parallel branches can't break main.
|
||||
# ENABLED -> global kill-switch (false -> whole gate is a no-op pass).
|
||||
# REPOS -> CSV of repos where the gate is REAL; empty -> only the self-hosting
|
||||
# repo (orchestrator); other repos -> conditional no-op (mirrors ORCH-35).
|
||||
# RETEST_TIMEOUT_S -> wall-clock budget for the post-rebase re-test.
|
||||
# RETEST_TARGET -> pytest target for the re-test.
|
||||
# LOCK_TIMEOUT_S -> max merge-lease age before a stale lease is reclaimed.
|
||||
# DEFER_DELAY_S -> delay before re-running the gate when the lock is busy.
|
||||
# DEFER_MAX_ATTEMPTS -> defer retries before escalation (avoids livelock).
|
||||
ORCH_MERGE_GATE_ENABLED=true
|
||||
ORCH_MERGE_GATE_REPOS=
|
||||
ORCH_MERGE_RETEST_TIMEOUT_S=600
|
||||
ORCH_MERGE_RETEST_TARGET=tests/
|
||||
ORCH_MERGE_LOCK_TIMEOUT_S=300
|
||||
ORCH_MERGE_DEFER_DELAY_S=60
|
||||
ORCH_MERGE_DEFER_MAX_ATTEMPTS=5
|
||||
# ORCH-026 Level A: unconditional pre-merge rebase. With the flag ON (default),
|
||||
# check_branch_mergeable ALWAYS rebases the branch onto origin/main under the held
|
||||
# merge-lease (not only when behind) — a deterministic structural anti-phantom on
|
||||
# the scheduler edge. No-op on an up-to-date branch (rebase keeps HEAD, force-with-
|
||||
# lease -> "Everything up-to-date", CI not triggered). Scope = ORCH_MERGE_GATE_REPOS.
|
||||
# PREMERGE_REBASE_ALWAYS=false -> strictly pre-ORCH-026 (rebase only when behind).
|
||||
ORCH_PREMERGE_REBASE_ALWAYS=true
|
||||
# ORCH-026 Level B: declarative task dependencies ("B waits for A"). claim_next_job
|
||||
# gates jobs whose depends-on tasks are not yet 'done' (additive job_deps table,
|
||||
# NOT EXISTS) WITHOUT occupying a max_concurrency slot. Inert on an empty job_deps.
|
||||
# TASK_DEPS_ENABLED=false -> claim query is 1:1 the ORCH-1 query (no gate).
|
||||
# TASK_DEPS_SOURCE=db|plane|hybrid -> declaration source; db (default) never calls
|
||||
# Plane on the hot path; plane/hybrid ingest Plane `blocked-by` relations and
|
||||
# cache them into job_deps (the scheduler then reads only the DB).
|
||||
ORCH_TASK_DEPS_ENABLED=true
|
||||
ORCH_TASK_DEPS_SOURCE=db
|
||||
# ORCH-088 (Stage 1, serial e2e): per-repo serial gate. A NEW task's analyst-job does
|
||||
# NOT enter analysis (no branch cut, no analyst) while the same repo has an EARLIER
|
||||
# unfinished task (FIFO, tasks.id < the job's task) OR the repo is frozen. The branch
|
||||
# cut is DEFERRED from start_pipeline to the analyst-job claim so its base is a fresh
|
||||
# origin/main already containing the predecessor (anti-stale-base). Gate lives in
|
||||
# claim_next_job (offline hot-path, fail-OPEN on error); freeze (FR-5) is a durable
|
||||
# repo_freeze row set on post-deploy DEGRADED, cleared manually via
|
||||
# POST /serial-gate/unfreeze?repo=<repo>. Leaf src/serial_gate.py (never-raise).
|
||||
# SERIAL_GATE_ENABLED=false -> claim AND start_pipeline are 1:1 as before ORCH-088.
|
||||
# SERIAL_GATE_REPOS (CSV) -> scope; EMPTY = ALL repos (not self-hosting-only).
|
||||
# SERIAL_GATE_FREEZE_ENABLED=false -> the rollback-freeze layer is off (not set/read).
|
||||
ORCH_SERIAL_GATE_ENABLED=true
|
||||
ORCH_SERIAL_GATE_REPOS=
|
||||
ORCH_SERIAL_GATE_FREEZE_ENABLED=true
|
||||
# ORCH-071/073: merge-verify under-gate on the `deploy -> done` edge (врезка in
|
||||
# advance_stage, NOT a new STAGE_TRANSITIONS edge / registered QG). A deterministic
|
||||
# merge-actor merges the feature code-PR via the Gitea PR-merge API (never push/
|
||||
# force-push to main), then `done` is allowed ONLY when the deployed SHA is proven an
|
||||
# ancestor of origin/main (ORCH-073 FR-1: SHA-in-main is the single criterion; a
|
||||
# merged PR alone no longer confirms). A secondary regression guard then checks a
|
||||
# declarative marker set (MAIN_REGRESSION_MARKERS) is still in origin/main; a missing
|
||||
# marker -> alert + HOLD (NOT done), a git error of the grep itself -> fail-open.
|
||||
# MERGE_VERIFY_ENABLED -> global kill-switch (false -> strictly pre-ORCH-071).
|
||||
# MERGE_VERIFY_REPOS -> CSV of repos where the under-gate is REAL; empty ->
|
||||
# only the self-hosting repo (orchestrator); non-self -> no-op.
|
||||
# MERGE_PR_TIMEOUT_S -> per Gitea list/merge HTTP call timeout.
|
||||
# MERGE_VERIFY_TIMEOUT_S -> git fetch/merge-base timeout for the ancestor + marker checks.
|
||||
# REGRESSION_GUARD_ENABLED -> kill-switch for the ORCH-073 main-integrity regression
|
||||
# guard (false -> SHA-in-main alone gates done); reuses the
|
||||
# merge-verify scope, so non-self repos are a no-op.
|
||||
# MERGE_VERIFY_AUTOCREATE_PR_ENABLED -> ORCH-082: guarantee an open code-PR
|
||||
# (head==branch, base==main) via merge_gate.ensure_open_pr
|
||||
# BEFORE the deterministic merge_pr (fixes the false HOLD
|
||||
# "no open PR"). false -> exactly pre-ORCH-082 behaviour.
|
||||
# Reuses the merge-verify scope; non-self repos -> no-op.
|
||||
ORCH_MERGE_VERIFY_ENABLED=true
|
||||
ORCH_MERGE_VERIFY_REPOS=
|
||||
ORCH_MERGE_PR_TIMEOUT_S=60
|
||||
ORCH_MERGE_VERIFY_TIMEOUT_S=60
|
||||
ORCH_REGRESSION_GUARD_ENABLED=true
|
||||
ORCH_MERGE_VERIFY_AUTOCREATE_PR_ENABLED=true
|
||||
# ORCH-036: executable self-deploy of the `deploy` stage. For the self-hosting repo
|
||||
# (orchestrator) the stage REALLY restarts prod (8500) via a detached host hook;
|
||||
# deploy_status: SUCCESS means proven health-ok, not an LLM declaration. Three
|
||||
# deterministic phases (A: request approve, B: human Approved -> detached deploy,
|
||||
# C: finalizer maps hook exit-code -> deploy_status). Non-self repos: unchanged
|
||||
# synchronous ssh deploy. SECRETS / host paths live ONLY on the host — do NOT commit.
|
||||
# SELF_DEPLOY_ENABLED -> global kill-switch (false -> legacy synchronous deploy for all).
|
||||
# SELF_DEPLOY_REPOS -> CSV of repos where Phase A/B/C is REAL; empty -> only the
|
||||
# self-hosting repo (orchestrator); others -> no-op (mirrors ORCH-35).
|
||||
# DEPLOY_REQUIRE_MANUAL_APPROVE -> require a human Plane "Approved" before the prod
|
||||
# deploy (true on rollout; full auto is ORCH-54).
|
||||
# DEPLOY_FINALIZE_DELAY_S -> delay before the first/each finalize poll (>= hook+health).
|
||||
# DEPLOY_FINALIZE_MAX_ATTEMPTS -> bounded finalize-defer budget (anti-livelock).
|
||||
# DEPLOY_SSH_USER / DEPLOY_SSH_HOST -> ssh target for the host hook (DEPLOY_SSH_HOST
|
||||
# empty -> detached deploy will NOT launch; set on the host).
|
||||
# DEPLOY_HOOK_SCRIPT -> path to the hook ON THE HOST (relative to the repo).
|
||||
# DEPLOY_HOST_REPO_PATH -> orchestrator clone path on the host.
|
||||
# DEPLOY_PROD_SOURCE_IMAGE -> staging-validated image, retagged build-once (no rebuild).
|
||||
# DEPLOY_PROD_TARGET_SERVICE / _PORT / _IMAGE / _COMPOSE_PROFILE -> prod compose profile.
|
||||
# DEPLOY_PROD_PREV_IMAGE_FILE -> prod prev-image snapshot (separate from staging's).
|
||||
ORCH_SELF_DEPLOY_ENABLED=true
|
||||
ORCH_SELF_DEPLOY_REPOS=
|
||||
ORCH_DEPLOY_REQUIRE_MANUAL_APPROVE=true
|
||||
ORCH_DEPLOY_FINALIZE_DELAY_S=90
|
||||
ORCH_DEPLOY_FINALIZE_MAX_ATTEMPTS=10
|
||||
ORCH_DEPLOY_SSH_USER=slin
|
||||
ORCH_DEPLOY_SSH_HOST=
|
||||
ORCH_DEPLOY_HOOK_SCRIPT=scripts/orchestrator-deploy-hook.sh
|
||||
ORCH_DEPLOY_HOST_REPO_PATH=/home/slin/repos/orchestrator
|
||||
ORCH_DEPLOY_PROD_SOURCE_IMAGE=orchestrator-orchestrator-staging
|
||||
ORCH_DEPLOY_PROD_TARGET_SERVICE=orchestrator
|
||||
ORCH_DEPLOY_PROD_TARGET_PORT=8500
|
||||
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
|
||||
# retries, unresolved sha->branch).
|
||||
# ENABLED -> global kill-switch (self-hosting safety / staged rollout).
|
||||
# PLANE_ENABLED -> separate flag for the F-2 Plane-API poll (mute only F-2).
|
||||
# INTERVAL_S -> background sweep period (seconds).
|
||||
# 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-068: TTL (seconds) for the per-project Plane states cache (plane_sync
|
||||
# _STATES_CACHE). Historically the cache lived for the whole process lifetime,
|
||||
# so a status added to Plane after start was invisible until a restart
|
||||
# ("stale set -> no pipeline action"). With a TTL the entry self-heals by
|
||||
# re-fetching /states/ once it expires (reuses reload_project_states()).
|
||||
# >0 -> re-fetch after this many seconds (default 300 = 5 min);
|
||||
# 0 -> disable TTL -> strictly the previous lifetime cache (back-compat).
|
||||
ORCH_PLANE_STATES_TTL_S=300
|
||||
|
||||
# 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-022: security-gate (secret-scanning + dependency audit) on the
|
||||
# deploy-staging -> deploy edge, run FIRST among the edge sub-gates. Deterministic
|
||||
# (no LLM): gitleaks (offline secret-scan, pinned Go binary in the image) + pip-audit
|
||||
# (OSV/PyPI CVE audit). Verdict in the versioned 17-security-report.md frontmatter;
|
||||
# FAIL -> rollback to development + developer-retry (cap 3). See ADR-001.
|
||||
# GATE_ENABLED -> global kill-switch; false -> pipeline 1:1 as before ORCH-022.
|
||||
# GATE_REPOS -> CSV of repos where the gate is REAL; empty -> only self-hosting.
|
||||
# DEP_BLOCK_SEVERITY -> CVE severity that BLOCKS (CRITICAL>HIGH>MEDIUM>LOW); below /
|
||||
# UNKNOWN -> warning only (anti-loop).
|
||||
# SCAN_TIMEOUT_S -> per external scanner call timeout.
|
||||
# DEP_AUDIT_FAIL_CLOSED -> strict mode: unreachable CVE feed -> FAIL instead of the
|
||||
# default fail-open + warning (anti-loop). Default false.
|
||||
# SECRETS_BLOCK -> a found secret blocks (always true by default; the offline
|
||||
# secrets guarantee is unconditional).
|
||||
ORCH_SECURITY_GATE_ENABLED=true
|
||||
ORCH_SECURITY_GATE_REPOS=
|
||||
ORCH_SECURITY_DEP_BLOCK_SEVERITY=HIGH
|
||||
ORCH_SECURITY_SCAN_TIMEOUT_S=300
|
||||
ORCH_SECURITY_DEP_AUDIT_FAIL_CLOSED=false
|
||||
ORCH_SECURITY_SECRETS_BLOCK=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
|
||||
|
||||
# ── QG-0 entry validation (ORCH-069) ──────────────────────────────────────────
|
||||
# Upper title-length limit for the QG-0 entry gate (_qg0_errors). The old 80-char
|
||||
# cap was a hygiene limit, not structural (slug is cut to [:30] independently, the
|
||||
# DB title TEXT is unbounded). Default 200. An invalid/empty value gracefully
|
||||
# degrades to 200 (the process never crashes on startup).
|
||||
ORCH_QG0_TITLE_MAX=200
|
||||
|
||||
@@ -50,3 +50,6 @@ ORCH_QUEUE_POLL_INTERVAL=2.0
|
||||
DEPLOY_SSH_USER=slin
|
||||
DEPLOY_SSH_HOST=127.0.0.1
|
||||
DEPLOY_HOOK_SCRIPT=/home/slin/bin/enduro-deploy-hook.sh
|
||||
|
||||
# QG-0 entry title-length limit (ORCH-069). Default 200; invalid/empty -> 200.
|
||||
ORCH_QG0_TITLE_MAX=200
|
||||
|
||||
13
.gitattributes
vendored
Normal file
13
.gitattributes
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# ORCH-073 (ADR-001 Р-5 / FR-4): union merge for the append-only changelog.
|
||||
#
|
||||
# CHANGELOG.md is append-only at the top (## [Unreleased]). Without a merge driver,
|
||||
# two branches that both add an Unreleased entry collide on auto_rebase_onto_main
|
||||
# (merge_gate), which rolls the branch back to `development` and can drag in stale
|
||||
# neighbouring code (a phantom-merge amplifier — see ADR-001 root cause #3). The
|
||||
# built-in `union` driver keeps BOTH sides' lines instead of conflicting, so both
|
||||
# changelog entries survive and the branch is not rolled back.
|
||||
#
|
||||
# Scope is INTENTIONALLY limited to CHANGELOG.md: `union` only suits strictly
|
||||
# append-only files. docs/**/*.md (README, ADR, internals) are rewritten line-by-line,
|
||||
# where `union` would silently duplicate edited lines — so they are NOT included.
|
||||
CHANGELOG.md merge=union
|
||||
38
.gitleaks.toml
Normal file
38
.gitleaks.toml
Normal file
@@ -0,0 +1,38 @@
|
||||
# gitleaks config — ORCH-022 security-gate (secret-scanning).
|
||||
#
|
||||
# Versioned in the repo root (07-infra I-4 / BR-13): rules + an allowlist of
|
||||
# known-safe matches are reviewed as code. The security-gate (src/security_gate.py)
|
||||
# passes this file via `--config` when present. gitleaks runs OFFLINE (local rules)
|
||||
# so the "a secret always blocks" guarantee (BR-2) never depends on the network.
|
||||
#
|
||||
# Strategy: extend the built-in ruleset (broad coverage, maintained upstream) and
|
||||
# only ADD a narrow allowlist for placeholders / fixtures that are intentionally
|
||||
# fake (e.g. .env.example dummy values, test fixtures). Keep the allowlist tight —
|
||||
# an over-broad allowlist silently re-opens the leak it was meant to bless.
|
||||
|
||||
title = "orchestrator gitleaks config"
|
||||
|
||||
[extend]
|
||||
# Start from gitleaks' maintained default ruleset.
|
||||
useDefault = true
|
||||
|
||||
[allowlist]
|
||||
description = "Known-safe, intentionally non-secret matches (placeholders + fixtures)."
|
||||
|
||||
# Files that legitimately contain placeholder/dummy secret-shaped values:
|
||||
# * .env.example — the committed canon of env vars with DUMMY values (CLAUDE.md §8;
|
||||
# real secrets live only in the host .env / .env.staging, never in git).
|
||||
# * tests/ — fixtures may embed fake tokens to exercise the scanner itself (TC-03).
|
||||
# * .gitleaks.toml — this file (avoid self-matching example patterns below).
|
||||
paths = [
|
||||
'''(^|/)\.env\.example$''',
|
||||
'''(^|/)tests/''',
|
||||
'''(^|/)\.gitleaks\.toml$''',
|
||||
]
|
||||
|
||||
# Generic placeholder tokens used in docs / examples that are NOT real secrets.
|
||||
regexes = [
|
||||
'''(?i)(your[-_]?(token|key|secret|password)[-_]?here)''',
|
||||
'''(?i)(changeme|dummy|example|placeholder|xxxxx+)''',
|
||||
'''(?i)<[a-z0-9_-]+>''',
|
||||
]
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: analyst
|
||||
description: Бизнес-аналитик. Создаёт пакет документов анализа для work item.
|
||||
model: claude-sonnet-4-6
|
||||
tools:
|
||||
- Filesystem (Read везде; Write только docs/work-items/<plane-id>/*)
|
||||
- Bash (git log, grep — только для чтения контекста)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: architect
|
||||
description: Архитектор системы. Принимает архитектурные решения по ТЗ, фиксирует как ADR.
|
||||
model: claude-opus-4-7
|
||||
tools:
|
||||
- Filesystem (Read везде; Write только docs/)
|
||||
- Bash (read-only: grep, git log)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: deployer
|
||||
description: DevOps-агент. Запускает staging-проверку и/или прод-деплой. Пишет 15-staging-log.md и 14-deploy-log.md.
|
||||
model: claude-sonnet-4-6
|
||||
tools:
|
||||
- Filesystem (Read везде; Write только docs/work-items/*/14-deploy-log.md, docs/work-items/*/15-staging-log.md)
|
||||
- Bash (docker, git, curl, ssh)
|
||||
@@ -37,8 +36,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
|
||||
@@ -73,13 +83,63 @@ On stage `deploy-staging` your job is to run the staging test suite and write a
|
||||
|
||||
---
|
||||
|
||||
## Stage: `deploy` (Production Deploy — ORCH-36, future)
|
||||
|
||||
On stage `deploy` your job is to perform (or simulate) the production deployment and write a machine-readable verdict to `docs/work-items/<work_item_id>/14-deploy-log.md` with frontmatter field `deploy_status: SUCCESS|FAILED`.
|
||||
## Stage: `deploy` (Production Deploy — ORCH-36, executable self-deploy)
|
||||
|
||||
This stage is only reached if the staging gate (`deploy-staging`) passed with `staging_status: SUCCESS`.
|
||||
The verdict contract is unchanged: `docs/work-items/<work_item_id>/14-deploy-log.md` with
|
||||
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.**
|
||||
|
||||
⚠️ **CRITICAL**: Do NOT trigger real production deploys unless explicitly instructed. Real docker/SSH deploys are handled by `scripts/orchestrator-deploy-hook.sh` (ORCH-36).
|
||||
### ⚠️ 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
|
||||
`src/stage_engine.py` + `src/self_deploy.py`, NOT by you, and NOT by a "paper" `SUCCESS`:
|
||||
|
||||
- **Phase A** (entering `deploy`): the pipeline does NOT launch you. It sets the issue to an
|
||||
approval-pending state and asks a human to flip the Plane status to **Approved**.
|
||||
- **Phase B** (human Approved): the code launches a **detached host process**
|
||||
(`ssh + setsid` → `scripts/orchestrator-deploy-hook.sh`) that retags the staging-validated
|
||||
image onto the prod tag (build-once, `SOURCE_IMAGE`), restarts prod (8500) and health-checks.
|
||||
The orchestrator NEVER restarts its own 8500 container from inside — that would kill the
|
||||
worker mid-call.
|
||||
- **Phase C** (finalizer): a deterministic finalizer-job in the NEW container reads the hook
|
||||
exit-code, maps `0 → SUCCESS`, `1|2|other → FAILED`, writes `14-deploy-log.md` and drives the
|
||||
existing contracts (`SUCCESS → done`, `FAILED → rollback to development`).
|
||||
|
||||
⚠️ **CRITICAL for self-hosting**: NEVER run `docker compose up -d orchestrator`, `--build`, or any
|
||||
restart of 8500 from inside the agent. `deploy_status: SUCCESS` must reflect a REAL host health-ok,
|
||||
never an LLM declaration. If you are ever launched on `deploy` for `orchestrator`, do nothing that
|
||||
restarts prod — the host hook owns the restart.
|
||||
|
||||
### Non-self repos (e.g. `enduro-trails`) — unchanged synchronous ssh deploy
|
||||
|
||||
For non-self repos behaviour is unchanged: perform the production deployment (ssh to the project
|
||||
host) and write the machine-readable verdict (`deploy_status: SUCCESS|FAILED`). Real docker/SSH
|
||||
deploys go through `scripts/orchestrator-deploy-hook.sh` (parametrised; defaults are STAGING-safe).
|
||||
|
||||
---
|
||||
|
||||
@@ -87,4 +147,7 @@ This stage is only reached if the staging gate (`deploy-staging`) passed with `s
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: developer
|
||||
description: Senior разработчик. Реализует ТЗ по ADR, пишет тесты, открывает PR.
|
||||
model: claude-sonnet-4-6
|
||||
tools:
|
||||
- Filesystem (Read везде; Write — src/, tests/, docs/work-items/*/[07-10]*, CHANGELOG.md)
|
||||
- Git (commit, push; merge запрещён)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: reviewer
|
||||
description: Senior code reviewer. Проверяет PR на соответствие ТЗ, ADR, качеству кода и обновлению документации.
|
||||
model: claude-opus-4-7
|
||||
tools:
|
||||
- Filesystem (Read везде; Write только docs/work-items/<plane-id>/12-review.md)
|
||||
- Git (read-only: log, diff, blame)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: tester
|
||||
description: QA-инженер. Прогоняет тесты, оформляет отчёт.
|
||||
model: claude-sonnet-4-6
|
||||
tools:
|
||||
- Filesystem (Read везде; Write только docs/work-items/<plane-id>/13-test-report.md)
|
||||
- Bash (pytest, curl)
|
||||
|
||||
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-088
|
||||
Repo: orchestrator
|
||||
Branch: feature/ORCH-088-orch-88-10-20
|
||||
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.
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
File diff suppressed because one or more lines are too long
52
CLAUDE.md
52
CLAUDE.md
@@ -6,8 +6,8 @@
|
||||
## Стек
|
||||
- Backend: FastAPI + uvicorn (Python 3.12)
|
||||
- БД: SQLite (`src/db.py`)
|
||||
- Агенты: Claude CLI (`ORCH_CLAUDE_BIN`), по одному промпту на роль в `.openclaw/agents/`
|
||||
- Очередь задач: собственная (SQLite `jobs`, `src/queue_worker.py`, ORCH-1)
|
||||
- Агенты: Claude CLI (`ORCH_CLAUDE_BIN`), по одному промпту на роль в `.openclaw/agents/`. **ORCH-74:** модель/эффорт агента берутся ТОЛЬКО из config (`resolve_agent_model`/`resolve_agent_effort`, ORCH-41) — frontmatter `model:` удалён как мёртвый, frontmatter описательный; имя модели валидируется форматом `^claude-…$` перед `--model` (never-break).
|
||||
- Очередь задач: собственная (SQLite `jobs`, `src/queue_worker.py`, ORCH-1). **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота `max_concurrency`; декларации/детект циклов — leaf `src/task_deps.py` (kill-switch `ORCH_TASK_DEPS_ENABLED`). Сериализация мержа одного репо — безусловный pre-merge rebase под merge-lease (`ORCH_PREMERGE_REBASE_ALWAYS`). **ORCH-088 (serial gate, Этап 1):** новая задача репо не входит в `analysis` (analyst-job не выбирается, ветка не режется), пока в репо есть **более ранняя** незавершённая задача (`t2.id < jobs.task_id`, FIFO) ИЛИ репо заморожен (`repo_freeze`). Срез ветки **отложен** со `start_pipeline` на момент claim analyst-job (`launcher._materialize_deferred_branch`) — база = свежий `origin/main` с кодом предшественника (анти-stale-base). Post-deploy `DEGRADED` → durable per-repo freeze (`repo_freeze`, `cleared_at IS NULL` = активен) + Telegram; снятие — вручную `POST /serial-gate/unfreeze?repo=…`. Leaf `src/serial_gate.py` (claim — fail-OPEN, freeze — fail-CLOSED); флаги `ORCH_SERIAL_GATE_ENABLED` (kill-switch), `ORCH_SERIAL_GATE_REPOS` (CSV; пусто = все репо), `ORCH_SERIAL_GATE_FREEZE_ENABLED`. Блок `serial_gate` в `GET /queue`. `STAGE_TRANSITIONS`/`QG_CHECKS` не тронуты.
|
||||
- Контейнеризация: Docker + Compose
|
||||
- CI/CD: Gitea Actions (`.gitea/workflows/`)
|
||||
- Деплой: docker compose на mva154
|
||||
@@ -38,16 +38,56 @@ 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`.
|
||||
|
||||
## Нотификации / Telegram live-tracker (ORCH-042/066/067/087)
|
||||
Каждая задача = **одна карточка** в Telegram (`src/notifications.py`). Поведение карточки:
|
||||
- **Дефолт `tracker_mode` — `bump`** (ORCH-067; `edit` доступен через `ORCH_TRACKER_MODE=edit`).
|
||||
`bump` на каждом обновлении удаляет старую карточку и шлёт свежую вниз чата (тихо), `edit`
|
||||
редактирует на месте. Инвариант «одна карточка на задачу» — в обоих режимах.
|
||||
- **Зачистка сирот (ORCH-087):** bump ведёт авторитетный леджер ВСЕХ созданных карточек
|
||||
(таблица `tracker_messages`, `deleted_at IS NULL` = жива) и на каждом обновлении удаляет
|
||||
ВСЕ незакрытые mid, а не только скаляр `tracker_message_id` (он сохранён как указатель на
|
||||
текущую карточку, BC). Это устраняет класс «замёрзшая сирота» (старая карточка с заголовком
|
||||
ранней стадии, потерявшая ссылку при гонке/`delete`-fail+`send`-ok). Новый mid пишется в
|
||||
леджер ТОЛЬКО при успешном `send` (BR-6); transient-`delete` остаётся незакрытым для ретрая;
|
||||
«already gone»/>48ч (`_DELETE_GONE_MARKERS`) → закрывается. Остаточная гонка самозалечивается
|
||||
за один bump. Known-limitation: Telegram 48ч (сироты старше неудаляемы).
|
||||
- **Эффорт в строке стадии (ORCH-087):** колонка `agent_runs.effort` стампится фактическим
|
||||
`resolve_agent_effort` в `launcher._spawn` (CLI его в result-JSON не возвращает); строка
|
||||
рендерится `· {model} · {effort}` (developer=`xhigh`, tester/deployer=`medium`, прочие=`high`);
|
||||
пустой/исторический effort → суффикс опускается.
|
||||
- **Честное итоговое время (ORCH-087):** done-строка = три независимых подписанных метрики
|
||||
`⏱️ Агенты {Σ agent_runs} · твоё {review~cap} · общее с ожиданием {wall}` (раньше `Всего {wall}`
|
||||
читалось как сумма, которой не является). «Твоё» ограничено `tracker_brd_review_cap_s`
|
||||
(`ORCH_TRACKER_BRD_REVIEW_CAP_S`, дефолт 2ч; маркер `~` при отсечке аномального застоя).
|
||||
- **Статус-строка карточки** (`📍 <status_label>`) показывает текущий Plane-статус по модели
|
||||
ORCH-066 (`plane_status_label`). Оффлайн-ядро (`stage → статус`, In Review из brd-clock)
|
||||
работает всегда без сети; best-effort live-overlay (kill-switch `tracker_live_status`,
|
||||
TTL-кэш, короткий таймаут) лишь дорисовывает ветки, неотличимые offline (Needs Input /
|
||||
Blocked / Rejected / Cancelled / **Confirm Deploy** / Deploying / Monitoring) и **никогда не
|
||||
блокирует конвейер**.
|
||||
- **Кликабельный номер задачи** (`plane_issue_link`) — `ORCH-NNN` в карточке И во всех
|
||||
уведомлениях (`notify_*`, alert'ы стадий) рендерится как `<a href=…>` на issue в Plane;
|
||||
fail-safe → просто `html.escape(номер)`, если ссылку построить нельзя. Никогда не падает.
|
||||
- **Без link-preview (ORCH-080):** оба примитива (`send_telegram`/`edit_telegram`) шлют
|
||||
payload с `disable_web_page_preview: True` — баннер Plane («Modern project management»)
|
||||
под кликабельной ссылкой `ORCH-NNN` больше не разворачивается ни в карточке (`bump`/`edit`),
|
||||
ни в notify/alert-сообщениях. `parse_mode: HTML` сохранён → ссылка остаётся кликабельной.
|
||||
- Транспорт (`send_telegram`/`edit_telegram`/`delete_telegram`), `disable_notification`
|
||||
(карточка тихая, пингуют только alert-хелперы), схема БД — не трогаются.
|
||||
|
||||
## Конвенции
|
||||
- Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`)
|
||||
- Ветки: `feature/ORCH-NNN-slug`, `fix/ORCH-NNN-slug`
|
||||
- ADR per work-item: `docs/work-items/<plane-id>/06-adr/ADR-NNN-slug.md`
|
||||
- Global ADR (сквозные решения): `docs/architecture/adr/adr-NNNN-slug.md`
|
||||
- Work items: `docs/work-items/<plane-id>/`
|
||||
- Машинные вердикты Quality Gate — строго YAML-frontmatter (`verdict:`, `deploy_status:`, `staging_status:`), никогда проза
|
||||
- Машинные вердикты Quality Gate — строго YAML-frontmatter (`verdict:`, `deploy_status:`, `staging_status:`, `security_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), `17-security-report.md` (security-гейт: `security_status:`/secrets/deps, ORCH-022).
|
||||
|
||||
## Правила для агентов
|
||||
1. Перед любым действием прочесть этот файл и `docs/architecture/README.md`.
|
||||
@@ -64,6 +104,10 @@ created → analysis → architecture → development → review → testing →
|
||||
- **НЕ перезапускать / не ронять прод-контейнер** `orchestrator` в рамках задачи — встанет конвейер всех проектов.
|
||||
- Любой деплой/рестарт self = групповой риск. Детали и топология — `docs/operations/INFRA.md`.
|
||||
- Стадия `deploy-staging` (порт 8501) — обязательная страховка перед прод-деплоем орка.
|
||||
- Прод-деплой орка запускается ТОЛЬКО переводом задачи на стадии `deploy` в выделенный
|
||||
Plane-статус **«Confirm Deploy»** (ORCH-059). Статус `Approved` — человеческий гейт
|
||||
конвейера и прод-деплой НЕ запускает (на `deploy` — no-op). Это разделяет «одобрить
|
||||
артефакт» и «выкатить в прод», чтобы привычный approve не ронял прод случайным кликом.
|
||||
|
||||
---
|
||||
*Паспорт проекта orchestrator. Поддерживается агентами при каждой доработке. Изолирован: описывает только этот проект (канон per-repo, см. ORCH-9).*
|
||||
|
||||
44
Dockerfile
44
Dockerfile
@@ -1,11 +1,51 @@
|
||||
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/*
|
||||
RUN apt-get update -qq && apt-get install -y -qq openssh-client git curl ca-certificates && 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-022: pinned gitleaks static Go binary for the offline secret-scan sub-gate
|
||||
# (07-infra I-1). Baked into the image (NOT a pip package): the gate runs INSIDE the
|
||||
# orchestrator container over a per-task worktree. Pinned release => deterministic
|
||||
# rules; gitleaks needs no network so the "a secret always blocks" guarantee (BR-2)
|
||||
# is independent of internet access. Multi-arch aware (amd64/arm64).
|
||||
ARG GITLEAKS_VERSION=8.18.4
|
||||
RUN set -eux; \
|
||||
arch="$(dpkg --print-architecture)"; \
|
||||
case "$arch" in \
|
||||
amd64) gl_arch="x64" ;; \
|
||||
arm64) gl_arch="arm64" ;; \
|
||||
*) echo "unsupported arch: $arch" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
curl -fsSL -o /tmp/gitleaks.tar.gz \
|
||||
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_${gl_arch}.tar.gz"; \
|
||||
tar -xzf /tmp/gitleaks.tar.gz -C /usr/local/bin gitleaks; \
|
||||
chmod +x /usr/local/bin/gitleaks; \
|
||||
rm -f /tmp/gitleaks.tar.gz; \
|
||||
gitleaks version
|
||||
# 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"]
|
||||
|
||||
@@ -121,6 +121,7 @@ uvicorn src.main:app --reload --port 8500
|
||||
| `ORCH_REPOS_DIR` | Repos dir (container) | `/repos` |
|
||||
| `ORCH_HOST_REPOS_DIR` | Repos dir (host) | `/home/slin/repos` |
|
||||
| `ORCH_DB_PATH` | SQLite path | `/app/data/orchestrator.db` |
|
||||
| `ORCH_RUNS_DIR` | Базовый каталог per-run логов агентов (`<runs_dir>/{run_id}.log`, ORCH-087) | `/app/data/runs` |
|
||||
| `ORCH_MAX_CONCURRENCY` | Сколько jobs воркер запускает параллельно (ORCH-1) | `1` |
|
||||
| `ORCH_QUEUE_POLL_INTERVAL` | Период опроса очереди воркером, сек (ORCH-1) | `2.0` |
|
||||
| `ORCH_PREFLIGHT_CACHE_TTL` | Кэш preflight (CLI/net), сек (ORCH-1 resilience) | `45` |
|
||||
@@ -129,6 +130,14 @@ uvicorn src.main:app --reload --port 8500
|
||||
| `ORCH_TRANSIENT_MAX_ATTEMPTS` | Ретраи для 429/недоступности | `5` |
|
||||
| `ORCH_BREAKER_THRESHOLD` | transient подряд до открытия breaker | `3` |
|
||||
| `ORCH_BREAKER_PAUSE_SECONDS` | Пауза при открытом breaker | `300` |
|
||||
| `ORCH_RECONCILE_ENABLED` | Kill-switch sweeper потерянных webhook (ORCH-053) | `true` |
|
||||
| `ORCH_RECONCILE_PLANE_ENABLED` | Отдельный флаг F-2 (опрос Plane API) | `true` |
|
||||
| `ORCH_RECONCILE_INTERVAL_S` | Период фонового прохода reconciler, сек | `120` |
|
||||
| `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_QG0_TITLE_MAX` | Верхний лимит длины заголовка QG-0 (вход `_qg0_errors`); невалидное/пустое значение → дефолт (ORCH-069) | `200` |
|
||||
|
||||
## Очередь задач (ORCH-1 / F-2b)
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@ services:
|
||||
build: .
|
||||
container_name: orchestrator
|
||||
restart: unless-stopped
|
||||
# ORCH-040: бежим под uid:gid хоста (slin=1000:1000), а не root, чтобы
|
||||
# артефакты конвейера (worktree + docs) создавались как slin:slin и git на
|
||||
# хосте работал без ручного chown. Доступ к docker.sock сохранён через
|
||||
# group_add: ["999"] (МИНА 1 — НЕ удалять). См. ADR-001 ORCH-040.
|
||||
user: "1000:1000"
|
||||
# init: true injects docker-init (tini) as PID 1 so reparented grandchild
|
||||
# processes from the claude/node subprocess tree are reaped (no zombies, B-2).
|
||||
init: true
|
||||
@@ -15,14 +20,21 @@ services:
|
||||
- /usr/bin/node:/usr/bin/node:ro
|
||||
- /home/slin/.claude:/home/slin/.claude
|
||||
- /home/slin/.claude.json:/home/slin/.claude.json:ro
|
||||
- /home/slin/.orchestrator-ssh:/root/.ssh:ro
|
||||
# ORCH-040: target согласован с HOME=/home/slin (launcher), не /root/.ssh.
|
||||
- /home/slin/.orchestrator-ssh:/home/slin/.ssh:ro
|
||||
env_file: .env
|
||||
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"
|
||||
|
||||
@@ -35,6 +47,8 @@ services:
|
||||
build: .
|
||||
container_name: orchestrator-staging
|
||||
restart: unless-stopped
|
||||
# ORCH-040: тот же uid хоста, что и у prod (см. комментарий выше / ADR-001).
|
||||
user: "1000:1000"
|
||||
init: true
|
||||
network_mode: host
|
||||
command: ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8501"]
|
||||
@@ -46,7 +60,8 @@ services:
|
||||
- /usr/bin/node:/usr/bin/node:ro
|
||||
- /home/slin/.claude:/home/slin/.claude
|
||||
- /home/slin/.claude.json:/home/slin/.claude.json:ro
|
||||
- /home/slin/.orchestrator-ssh:/root/.ssh:ro
|
||||
# ORCH-040: target согласован с HOME=/home/slin (launcher), не /root/.ssh.
|
||||
- /home/slin/.orchestrator-ssh:/home/slin/.ssh:ro
|
||||
env_file: .env.staging
|
||||
environment:
|
||||
- ORCH_REPOS_DIR=/repos
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
- **Stage Engine** (`src/stage_engine.py`) — исполнение переходов, диспетчеризация QG (`_run_qg`), откаты, синхронизация с Plane.
|
||||
- **Review/Test Parsers** (`src/review_parse.py`, ORCH-046) — defensive-извлечение дословного must-fix текста из артефактов для встраивания в `task_desc` заворота: `extract_review_findings` (P0/P1 из `12-review.md`), `extract_test_failures` (фрагмент тела `13-test-report.md`). Контракт «never raise»: любая ошибка → `""`.
|
||||
- **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.
|
||||
- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance. Модель/эффорт каждого агента резолвятся из config (`resolve_agent_model`/`resolve_agent_effort`, ORCH-41), а не из frontmatter промпта. **ORCH-74:** имя модели валидируется форматом `^claude-…$` (`is_valid_model`) перед `--model`; невалидное → лог + откат на следующий уровень/CLI-дефолт (never-break, как `VALID_EFFORTS` для эффорта). Тот же предикат гардит inline-чтение `--fallback-model`.
|
||||
- **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe. **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота; декларации/циклы — leaf `src/task_deps.py`.
|
||||
- **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`.
|
||||
- **Notifications / Live-tracker** (`src/notifications.py`, ORCH-042/ORCH-067) — ОДНА live-карточка на задачу (`update_task_tracker`), обновляется на каждом переходе. Режим `ORCH_TRACKER_MODE` (дефолт `bump` с ORCH-067: delete+silent send+repoint внизу чата; `edit` — правка на месте). Карточка несёт строку Plane-статуса `📍 …` (оффлайн-ядро `plane_status_label` + best-effort live-overlay `_live_plane_branch_override`, kill-switch `ORCH_TRACKER_LIVE_STATUS`) и кликабельный номер задачи (`plane_issue_link`/`link_for` → ссылка в Plane, fail-safe на сырой номер). **ORCH-080:** оба низкоуровневых примитива (`send_telegram`/`edit_telegram`) шлют payload с `disable_web_page_preview: True` — Telegram больше не разворачивает баннер link-preview Plane под карточкой/уведомлениями; `parse_mode: HTML` сохранён (ссылка остаётся кликабельной), безусловно без kill-switch. Все алерты, упоминающие `work_item_id`, делают номер кликабельным. **ORCH-087:** bump ведёт авторитетный леджер всех созданных карточек (`tracker_messages`, `deleted_at IS NULL` = жива) и на каждом обновлении зачищает ВСЕ незакрытые mid (а не только скаляр `tracker_message_id`) → класс «замёрзшая сирота» устранён; строка стадии несёт фактический эффорт рядом с моделью (`· {model} · {effort}`, колонка `agent_runs.effort`, стамп в `launcher._spawn`); done-строка времени переписана на три подписанных метрики `⏱️ Агенты · твоё{~cap} · общее с ожиданием` (кап `ORCH_TRACKER_BRD_REVIEW_CAP_S`); deploy-цикл дополнен overlay-ключом `confirm_deploy`. Контракт всего компонента — never raises; карточка всегда silent. Детали — [internals.md](internals.md) §7 и [ADR-001](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md).
|
||||
- **Project Registry** (`src/projects.py`, ORCH-6) — Plane project id → repo + prefix; фильтрация вебхуков по проекту.
|
||||
- **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane.
|
||||
- **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane. Резолв статусов проекта `get_project_states` (ORCH-10) кэширует `{logical_key→uuid}` per-project; **ORCH-068** добавляет в кэш-запись `{uuid→group}` (для терминал-исключения F-2) и **TTL** `ORCH_PLANE_STATES_TTL_S` (дефолт 300с; `0` → прежний lifetime-кэш) — устаревший набор статусов самозалечивается без рестарта процесса через существующий `reload_project_states()` (баг кэша после появления нового Plane-статуса). Форма возврата `get_project_states` неизменна (обратная совместимость).
|
||||
|
||||
## Конвейер и Quality Gates
|
||||
|
||||
@@ -34,17 +37,538 @@ 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.
|
||||
**Реестр 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), check_security_gate (ORCH-022).
|
||||
|
||||
**Канон гейтов:** машинные вердикты читаются ТОЛЬКО из YAML-frontmatter, никогда из прозы. Лог-файлы мержатся в `origin/main` отдельным PR; гейт читает из `origin/main`.
|
||||
|
||||
### Модель и эффорт по ролям (ORCH-41, валидация ORCH-74)
|
||||
Модель и `--effort` каждого агента берутся из config (`src/config.py`), резолвятся `launcher.resolve_agent_model` / `resolve_agent_effort` по приоритету **project-override (`projects_json` `agent_models`/`agent_efforts`) > `ORCH_AGENT_MODEL_<AGENT>`/`ORCH_AGENT_EFFORT_<AGENT>` > `*_default` > CLI-дефолт (без флага)**. **Эффорт (ORCH-081):** ниже `*_default` добавлен непустой **per-role floor** — class-default поля `agent_effort_<role>` из `config.py` (его пустой env перебить не может). Floor — строго последний уровень (ниже default) и срабатывает ТОЛЬКО когда все уровни пусты, поэтому пустые прод-`ORCH_AGENT_EFFORT_*=` (которые pydantic трактует как явное `''` и обнуляют дефолт) больше не приводят к запуску без `--effort`: каждая роль получает свой канонический пол (developer=`xhigh`, tester/deployer=`medium`, прочие=`high`). Непустой явный конфиг по-прежнему побеждает floor; опечатка вне `VALID_EFFORTS` дропается валидацией ДО floor (never-break, не маскируется). См. `docs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md`. frontmatter `model:` в `.openclaw/agents/*.md` **удалён** (ORCH-74 G1) — он был мёртвой/лживой декларацией (launcher его не читает); config — единственный источник правды о модели. Model-routing (G3) НЕ включён — все 6 агентов на `claude-opus-4-8`.
|
||||
|
||||
| Агент | Модель | Эффорт |
|
||||
|-------|--------|--------|
|
||||
| analyst | claude-opus-4-8 | high |
|
||||
| architect | claude-opus-4-8 | high |
|
||||
| developer | claude-opus-4-8 | xhigh |
|
||||
| reviewer | claude-opus-4-8 | high |
|
||||
| tester | claude-opus-4-8 | medium |
|
||||
| deployer | claude-opus-4-8 | medium |
|
||||
|
||||
**Валидация (ORCH-74 G2, never-break):** резолвенное имя модели проходит формат-чек `is_valid_model` (`^claude-[a-z0-9.-]+$`) перед попаданием в `--model`. Невалидное (опечатка, `gpt-4`, пустое) → `logger.warning` + откат на следующий валидный уровень (в пределе — без `--model`, CLI-дефолт); мусор **никогда** не уезжает в CLI и запуск не падает. Форма — формат-чек, а не статичный allowlist: forward-compatible (будущие `claude-*` проходят без правки кода). Тот же предикат гардит inline-чтение `--fallback-model` (`agent_fallback_model` читается мимо резолва — TRZ §4). Эффорт валидируется множеством `VALID_EFFORTS` (`low|medium|high|xhigh|max`). Fallback (G4) НЕ включён (`agent_fallback_model=""`). Детали — `docs/work-items/ORCH-074/06-adr/ADR-001-model-name-validation.md`.
|
||||
|
||||
### Условный 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 завершился»).
|
||||
|
||||
Назначение: ветка валидируется относительно того `main`, из которого создана; параллельная задача могла уйти вперёд → семантический конфликт слияния (зелёная ветка ломает обновлённый `main`). Merge-gate гарантирует проверку против **актуального** `origin/main` перед слиянием:
|
||||
- **Догон:** ветка отстаёт (⇔ `origin/main` не предок HEAD) → `rebase origin/main` в worktree + `push --force-with-lease` (ТОЛЬКО ветка задачи; `main` — никогда). Текстовый конфликт → `rebase --abort` → откат на `development`.
|
||||
- **Безусловный pre-merge rebase (ORCH-026, A-2):** при `premerge_rebase_always` (дефолт `True`, скоуп `merge_gate_repos`) short-circuit `branch_is_behind_main` пропускается — `auto_rebase_onto_main` вызывается **всегда** под лизом. На актуальной ветке это no-op (`rebase` не меняет HEAD, `push --force-with-lease` → «Everything up-to-date», CI не триггерится); на отстающей — реальный догон. Детерминированный структурный анти-фантом на уровне планировщика (дополняет рубежи ORCH-073, не заменяет). Kill-switch `premerge_rebase_always=False` → прежнее поведение (ребейз только при behind).
|
||||
- **Re-test:** `python -m pytest` (`merge_retest_target`, дефолт `tests/`) в worktree догнанной ветки, тайм-аут `merge_retest_timeout_s`. Красный/тайм-аут → откат на `development`.
|
||||
- **Сериализация (merge-lock):** файловый **merge-lease** на репо (`<repos_dir>/.merge-lease-<repo>.json`), живёт от гейта до фактического merge. Acquire **неблокирующий** (anti-deadlock при `max_concurrency=1`): busy → **defer** (повторная постановка deployer'а на `deploy-staging` с задержкой через `available_at`), а не откат. Release — на PR-merged вебхуке / `deploy→done` / откате / по возрасту (crash-реклейм). Restart-safe; без изменения схемы БД. **ORCH-026 (A-1):** это окно = «merge → main-updated» (для self `done` ⇔ SHA-in-main, ORCH-073) — пока A не в `main`, B того же репо получает `merge-lock busy` → defer. Окно сериализации per-repo НЕ переписывается; кросс-репо параллелизм сохранён (лиз — per-repo файл).
|
||||
- **Условность (как ORCH-35):** реален для `orchestrator`; прочие репо — no-op. Флаги `merge_gate_enabled` / `merge_gate_repos` — поэтапный раскат. Контракт **never-raise**.
|
||||
|
||||
Подробнее: [adr-0006](adr/adr-0006-merge-gate.md), детально — `docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md`.
|
||||
Безусловный pre-merge rebase + связь с зависимостями задач — [adr-0015](adr/adr-0015-task-deps-and-merge-serialization.md) (ORCH-026).
|
||||
|
||||
### Зависимости задач: B ждёт A (ORCH-026, Уровень B)
|
||||
Плоская очередь ORCH-1 (FIFO по `id` + `available_at` + `max_concurrency`) не выражала логических зависимостей. ORCH-026 вводит декларативные связи «задача B не стартует, пока не готовы её depends-on» — без новой стадии и без изменения `STAGE_TRANSITIONS`/`QG_CHECKS`.
|
||||
- **Источник истины планировщика — БД** (аддитивная таблица `job_deps(task_id, depends_on_task_id)`): claim в горячем цикле обслуживает очередь ВСЕХ проектов и обязан быть offline-устойчив (сетевой Plane на каждый claim = встанет очередь всех проектов). Источник **декларации** настраивается `task_deps_source = db|plane|hybrid` (дефолт `db`; `plane`/`hybrid` читают Plane relations в `handle_work_item_created` и кэшируют в `job_deps`).
|
||||
- **Гейт планировщика (`claim_next_job`)** — условие `NOT EXISTS (job_deps d JOIN tasks t … WHERE d.task_id=j.task_id AND t.stage!='done')` при `task_deps_enabled`: задача с незавершённой зависимостью **не выбирается** (агент не запускается, слот `max_concurrency` не занимается). Инертно при пустой `job_deps` → нулевая регрессия; kill-switch `task_deps_enabled=False` → запрос 1:1 как ORCH-1.
|
||||
- **Детект дедлоков** — DFS-цикл-детектор (leaf `src/task_deps.py::detect_cycle`) при вставке связи + backstop в `reconciler`; цикл → `set_issue_blocked` + alert (Telegram/Plane) с перечислением цикла. Поток остальных задач не блокируется.
|
||||
- **Видимость** — строка «⏳ ждёт ORCH-NNN» в Telegram-карточке (`update_task_tracker`, never-raise); Plane `Blocked` — на дедлоке (не на нормальном коротком ожидании, чтобы не флаппить). Инвариант «одна карточка на задачу» сохранён.
|
||||
- **Совместимость:** `reconciler` F-1 пропускает dep-заблокированные задачи (`is_task_ready`, паттерн ORCH-060); `reaper` сканирует только `running` → dep-блок остаётся `queued`, не трогается. Зависимости — только intra-repo (v1).
|
||||
- **Наблюдаемость:** блок `task_deps` в `GET /queue` (заблокированные задачи, держатель merge-lease, defer-счётчики, обнаруженные циклы) — read-only.
|
||||
|
||||
Подробнее: [adr-0015](adr/adr-0015-task-deps-and-merge-serialization.md), детально — `docs/work-items/ORCH-026/06-adr/ADR-001-merge-serialization-and-task-deps.md`.
|
||||
|
||||
### Per-repo serial gate: пакетный автономный режим (ORCH-088 — реализовано)
|
||||
Эпик «10–20 задач за ночь», Этап 1 (serial e2e). Закрывает **stale-анализ**: ветка задачи N+1
|
||||
срезалась на входе в анализ (`start_pipeline._create_gitea_branch`) от `main`, ещё не содержащего код
|
||||
предшественника N (физическое код-затирание уже закрыто ORCH-026; ORCH-088 — **логический** разрыв).
|
||||
Новая задача репо не входит в `analysis` (не режет ветку, не запускает analyst), пока в том же репо
|
||||
есть незавершённая задача (`stage != 'done'`) или репо заморожен. Аддитивно, под kill-switch, область
|
||||
репо, never-raise, restart-safe; `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` — **без изменений**.
|
||||
- **Gate-в-claim** (`db.claim_next_job`) — analyst-job (`jobs.agent='analyst'`) применимого репо не
|
||||
выбирается, если `EXISTS` **более ранняя** незавершённая задача репо (`t2.id < jobs.task_id`) ИЛИ
|
||||
активна строка `repo_freeze`. По образцу `task_deps` `NOT EXISTS` (ORCH-026); только локальная БД
|
||||
(offline hot-path, NFR-2). Job'ы уже активной задачи проходят свободно. **FIFO-уточнение реализации
|
||||
(FR-2):** ADR-001 D1 фиксировал псевдо-SQL `t2.id != jobs.task_id`; при `!=` пакет одновременно
|
||||
созданных свежих задач (все в `analysis`) взаимно блокировался бы (каждая — «другая незавершённая»
|
||||
для остальных) ⇒ дедлок всей serial-очереди. `<` допускает ровно самую раннюю задачу и сериализует
|
||||
остальные за ней (строго по одной, FIFO по `jobs.id`), при этом по-прежнему не блокирует rework-analyst
|
||||
собственной задачи (R-7) и сохраняет AC-1.
|
||||
- **Отложенный срез ветки (анти-stale-base, AC-6):** для применимого репо `start_pipeline` создаёт
|
||||
task-row + enqueue analyst, но **не** создаёт Gitea-ветку/docs; срез релоцируется на момент claim
|
||||
analyst-job (launcher), когда `origin/main` уже содержит предшественника (`done` ⇔ SHA-в-main,
|
||||
ORCH-071/073). `ensure_worktree` режет от свежего `origin/main` ⇒ AC-6 структурно. Идемпотентно
|
||||
(`_create_gitea_branch` 409 = no-op).
|
||||
- **Durable per-repo freeze** (новая аддитивная таблица `repo_freeze`, `cleared_at IS NULL` = активен) —
|
||||
post-deploy `DEGRADED`/rollback (ORCH-021) → `set_repo_freeze` + Telegram-алерт; gate закрыт
|
||||
безусловно до **ручного** снятия (`POST /serial-gate/unfreeze`). Деградировавшая задача уже `done`
|
||||
(BR-7) ⇒ отдельный сигнал, независимый от `stage`.
|
||||
- **Согласование NFR-1:** hot-claim тотальный сбой построения gate-фрагмента → **fail-open** (не
|
||||
заклинить очередь всех проектов, AC-8); freeze в Python-слое (`is_repo_frozen`) → **fail-closed**
|
||||
(безопасность прода, AC-9).
|
||||
- Чистая логика — leaf `src/serial_gate.py` (never-raise). Флаги `serial_gate_enabled` (kill-switch),
|
||||
`serial_gate_repos` (CSV; **пусто ⇒ все репо**, в отличие от self-hosting-only ORCH-35/43/58),
|
||||
`serial_gate_freeze_enabled`. Наблюдаемость — аддитивный блок `serial_gate` в `GET /queue`
|
||||
(per-repo `active_task` / `waiting` / `frozen`). Cross-repo параллелизм сохранён (FR-3); при
|
||||
выключенном флаге — нулевая регрессия (enduro не затронут).
|
||||
|
||||
Подробнее: [adr-0017](adr/adr-0017-serial-gate.md), детально —
|
||||
`docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`,
|
||||
`docs/work-items/ORCH-088/08-data-requirements.md`.
|
||||
|
||||
### Исполняемый самодеплой стадии `deploy` (ORCH-36)
|
||||
`deploy` перестаёт быть «бумажной»: для self-hosting (`is_self_hosting_repo`) стадия
|
||||
РЕАЛЬНО деплоит прод (8500) через хост-хук `scripts/orchestrator-deploy-hook.sh`,
|
||||
а `deploy_status: SUCCESS` означает доказанный health-ok, не декларацию LLM. Три фазы
|
||||
(детерминированно, без LLM в критическом пути self-restart):
|
||||
- **Фаза A (вход в `deploy`)** — при `deploy_require_manual_approve=true` вместо запуска
|
||||
прод-deployer выставляется approval-pending статус Plane + запрос перевести задачу
|
||||
в статус **«Confirm Deploy»** (ORCH-059; Plane-коммент + Telegram). Перехват в
|
||||
`advance_stage` ПОСЛЕ `check_staging_status` и merge-gate.
|
||||
- **Фаза B (Plane → `Confirm Deploy`, ORCH-059)** —
|
||||
`advance_stage(deploy, finished_agent=None, confirm_deploy=True)`
|
||||
запускает **detached host-процесс** (ssh + setsid → хук с прод-параметрами +
|
||||
build-once retag `SOURCE_IMAGE`) и ставит детерминированный **finalizer-job**;
|
||||
маркер `initiated` — идемпотентность. Возврат БЕЗ advance (вердикта ещё нет).
|
||||
Обычный `Approved` на `deploy` (`confirm_deploy=False`) — детерминированный no-op
|
||||
(не деплоит и не откатывает).
|
||||
- **Фаза C (finalizer)** — новый контейнер после рестарта читает sentinel `result`
|
||||
(exit-code хука), маппит `0→SUCCESS / иначе→FAILED`, пишет `14-deploy-log.md`,
|
||||
вызывает `advance_stage(deploy, finished_agent="deployer")` → существующие контракты:
|
||||
`SUCCESS → done`, `FAILED → откат БАГ-8 на development`.
|
||||
|
||||
Триггер прод-деплоя = смена статуса Plane на `Confirm Deploy` (ORCH-059; status-only
|
||||
verdict model; комментарии не управляют конвейером). `Approved` остаётся исключительно
|
||||
человеческим гейтом конвейера и прод-деплой не запускает. На старте — обязательный
|
||||
ручной approve (флаг `true`); полный авто — отдельная задача (ORCH-54). Условность как ORCH-35: реально для `orchestrator`,
|
||||
прочие репо — прежний синхронный ssh-деплой агентом. Контракты не меняются:
|
||||
`STAGE_TRANSITIONS`, реестр QG, `check_deploy_status`/`_parse_deploy_status`, БАГ-8,
|
||||
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`.
|
||||
|
||||
#### Выделенный статус-триггер прод-деплоя «Confirm Deploy» (ORCH-059 — реализовано)
|
||||
Перегрузка: один Plane-статус `Approved` служил И человеческим гейтом BRD на
|
||||
`analysis` (`check_analysis_approved`), И триггером Фазы B прод-деплоя на `deploy`
|
||||
— привычный жест approve молча запускал прод-рестарт (групповой self-hosting
|
||||
риск). ORCH-059 разделяет жесты: вводится отдельный логический статус
|
||||
`confirm_deploy` («Confirm Deploy»), который триггерит **ТОЛЬКО** Фазу B на
|
||||
`deploy`; `Approved` остаётся исключительно гейтом конвейера.
|
||||
- `_PLANE_NAME_TO_KEY` += `"Confirm Deploy" → "confirm_deploy"`; в
|
||||
`_DEFAULT_STATES` ключ НЕ добавляется (нет UUID для enduro/fallback) →
|
||||
**fail-closed**: нет статуса → нет деплоя, без `KeyError` (доступ через `.get`).
|
||||
- `handle_issue_updated` маршрутизирует `Confirm Deploy` → `handle_confirm_deploy`
|
||||
(гард `stage=="deploy"`) → `_try_advance_stage(..., confirm_deploy=True)`.
|
||||
- `advance_stage` получает kwarg `confirm_deploy: bool=False`; блок Фазы B
|
||||
(`deploy`+`finished_agent is None`+self-hosting) деплоит ТОЛЬКО при
|
||||
`confirm_deploy=True`, иначе (обычный `Approved`) — **no-op** (`check_deploy_status`
|
||||
не запускается → нет ложного отката БАГ-8).
|
||||
- CTA Фазы A (`_handle_self_deploy_phase_a`) просит «Confirm Deploy», не «Approved».
|
||||
- Условность как ORCH-35/36 (только `orchestrator`); Фазы A/C, `STAGE_TRANSITIONS`,
|
||||
`QG_CHECKS`, `check_deploy_status`, merge-gate, схема БД — без изменений.
|
||||
- Эксплуатация: в Plane-проекте ORCH создать статус «Confirm Deploy» + сброс кэша
|
||||
состояний (`docs/work-items/ORCH-059/07-infra-requirements.md`).
|
||||
|
||||
Детально — `docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md`
|
||||
(уточняет/триггер Фазы B относительно adr-0007).
|
||||
|
||||
#### Merge-в-main + пост-деплой верификация как условие `done` (ORCH-071 — фикс фантомного merge)
|
||||
**Фантомный merge** (CRITICAL, постмортем `docs/history/LESSONS_2026-06-08_phantom-merge.md`):
|
||||
на self-hosting пути `deploy` агент `deployer` НЕ запускается, а фактический merge PR в `main`
|
||||
исторически делал ТОЛЬКО он → детерминированный путь
|
||||
(`_handle_self_deploy_phase_b → initiate_deploy → run_deploy_finalizer`) **не содержал шага
|
||||
merge-в-main вообще**. Detached host-деплой лишь retag'ал образ + рестартил 8500; `done`
|
||||
достигался по `deploy_status: SUCCESS` без верификации `main`. Зелёный деплой (образ из рабочей
|
||||
ветки) маскировал отсутствие merge → следующая задача срезала ветку от устаревшего `main` и
|
||||
теряла код предшественника (накопительно потеряны ORCH-022/059/066/068). ORCH-071 вводит
|
||||
**детерминированный merge-актор + пост-merge верификацию** как **под-гейт ребра `deploy → done`**
|
||||
(симметрично edge-под-гейтам `deploy-staging → deploy`), только для self-hosting:
|
||||
- **Врезка `_handle_merge_verify` в `advance_stage`** (`current_stage=="deploy"` и
|
||||
`next_stage=="done"`, ПОСЛЕ зелёного `check_deploy_status`, ДО `update_task_stage`). Гейтит
|
||||
**ВСЕ** пути к `done` единообразно (`run_deploy_finalizer` Phase C, reconciler F-1, job-reaper —
|
||||
все идут через `advance_stage`), закрывая дыру обхода merge.
|
||||
- **Merge в Phase C (после рестарта), НЕ в Phase B** — finalizer restart-surviving (claim воркером
|
||||
нового контейнера, re-drive reaper'ом), merge физически строго ПОСЛЕ рестарта прода → рестарт его
|
||||
не убивает (G3 «шаг, переживающий рестарт»; постмортем-урок №3).
|
||||
- **Merge-актор `merge_gate.merge_pr`** — `pr_already_merged` (idempotency no-op повтор) → иначе
|
||||
Gitea `POST /repos/{owner}/{repo}/pulls/{index}/merge`. Выбор PR строго по `head.ref==branch`
|
||||
И `base.ref=="main"`. Никогда push/force-push в `main`.
|
||||
- **Верификатор `merge_gate.verify_merged_to_main` (семантика ORCH-073, FR-1):** подтверждение —
|
||||
**ТОЛЬКО** `git merge-base --is-ancestor <validated_sha> origin/main` (`validated_revision` —
|
||||
якорь ORCH-058). PR-флаг `pr_already_merged` **больше НЕ подтверждает merge** (удалён из verify):
|
||||
он понижен до idempotency-guard `merge_pr` и засчитывает merged PR лишь при `head.ref==branch`
|
||||
И `base.ref=="main"` (исключает авто docs-PR). Пустой SHA / git-ошибка → `False` (fail-closed),
|
||||
never-raise.
|
||||
- **Регресс-гард целостности `main` (ORCH-073, FR-5):** `merge_gate.check_main_regression` в
|
||||
`_handle_merge_verify` ПОСЛЕ подтверждённого SHA-в-main и ДО `done` проверяет, что `origin/main`
|
||||
содержит декларативный набор маркеров ранее-merged задач (`MAIN_REGRESSION_MARKERS`,
|
||||
`git grep -c <marker> origin/main -- <path>` > 0). Маркер отсутствует → alert «main regressed» +
|
||||
HOLD (НЕ `done`, ALERT-only). Fail-open на git-ошибке грепа (регресс — только при `count==0`).
|
||||
Kill-switch `regression_guard_enabled`; non-self → no-op. Набор — append-only константа,
|
||||
значимая задача дописывает свой маркер.
|
||||
- **Не подтверждено → alert «deploy succeeded but not merged» (Telegram+Plane) + HOLD**
|
||||
(`set_issue_blocked`, задача НЕ `done`, БЕЗ авто-отката на `development` — not-merged есть
|
||||
инфра-дефект, реакция ALERT-only как ORCH-021 self-hosting). Подтверждено → штатный `deploy →
|
||||
done` + `merged_to_main: true` во frontmatter `14-deploy-log.md` (`deploy_status:` нетронут).
|
||||
- **Защита от CHANGELOG-затирания (ORCH-073, FR-4):** корневой `.gitattributes` с
|
||||
`CHANGELOG.md merge=union` → правки `## [Unreleased]` авто-сливаются при `auto_rebase_onto_main`
|
||||
без конфликта, ветка не откатывается в `development` и не тащит устаревший код-сосед. `docs/**`
|
||||
под union НЕ ставится (union только для append-only).
|
||||
- **Условность как ORCH-35/43/58:** `merge_verify_enabled` (kill-switch, дефолт `true`) +
|
||||
`merge_verify_repos` (пусто → только self-hosting); non-self — no-op, merge остаётся за `deployer`.
|
||||
never-raise; идемпотентность по **SHA-в-main** (INV-4, не «любой merged PR»); ручной approve
|
||||
сохранён (`Confirm Deploy`).
|
||||
- **Инварианты:** `STAGE_TRANSITIONS`, `check_deploy_status`/`_parse_deploy_status`, реестр
|
||||
`QG_CHECKS` (под-гейт — врезка в `advance_stage`, НЕ новый зарегистрированный QG), схема БД,
|
||||
БАГ-8, terminal-sync, merge-gate, image-freshness, exit-коды хука — **без изменений**.
|
||||
Диагностика фантома — runbook `docs/operations/PHANTOM_MERGE_RUNBOOK.md` (4 проверки постмортема).
|
||||
|
||||
Подробнее: [adr-0013](adr/adr-0013-merge-verify-gate.md) +
|
||||
[adr-0014](adr/adr-0014-merge-verify-sha-source-of-truth.md) (amends 0013 — SHA-в-main как
|
||||
единственный критерий + регресс-гард, ORCH-073); детально —
|
||||
`docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md`,
|
||||
`docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md`.
|
||||
|
||||
#### Гарантированный код-PR перед merge-verify (ORCH-082 — фикс ложного HOLD «no open PR»)
|
||||
Под-гейт merge-verify (ORCH-071/073) детерминированно мержит **открытый** код-PR ветки в `main`
|
||||
(`merge_pr`, фильтр `head.ref==branch` И `base.ref=="main"`). Но конвейер **не гарантировал**, что
|
||||
к моменту merge у ветки этот PR есть: PR создаётся единственной `launcher._ensure_pr` **только** на
|
||||
developer-пути и **только** при свежем worktree-коммите. На деплое ORCH-074 (08.06, первая задача
|
||||
после ручных восстановлений `main`) у ветки не оказалось открытого код-PR → `merge_pr` вернул
|
||||
`("False", "no open PR")` → защита ORCH-073 верно удержала задачу (HOLD, не ложный `done`), но это
|
||||
лечило следствие. ORCH-082 закрывает **отсутствующий инвариант** «к merge-verify у ветки есть
|
||||
открытый код-PR» аддитивно, внутри того же под-гейта, не трогая машину стадий:
|
||||
- **Новый leaf-актор `merge_gate.ensure_open_pr(repo, branch) -> (status, detail)`** (never-raise):
|
||||
`GET …/pulls?state=open` с фильтром `head.ref==branch` И `base.ref=="main"` (**идентичен**
|
||||
`merge_pr`/ORCH-073 FR-3 — авто-docs-PR `base != main` НЕ код-PR) → `("existed", N)`; иначе
|
||||
`POST …/pulls` → `("created", N)`; гонка «PR exists»/409/422 → повторный GET → `existed` (без
|
||||
дублей); любая иная ошибка → `("failed", reason)`.
|
||||
- **Врезка в `_handle_merge_verify`** ПОСЛЕ резолва `validated_revision` и **ПЕРЕД** `merge_pr`:
|
||||
`created|existed` → штатно к `merge_pr` → `verify_merged_to_main`; `failed` → честный HOLD+alert
|
||||
через новый helper `_hold_pr_create_failed` (текст «PR создать не удалось» — отличим от
|
||||
not-merged HOLD; `result.note="pr-create-failed-hold"`), задача остаётся на `deploy`, БЕЗ отката
|
||||
на development.
|
||||
- **Защита ORCH-073 неприкосновенна и приоритетна:** подтверждение merge остаётся ТОЛЬКО
|
||||
`verify_merged_to_main` (SHA-в-main) + `check_main_regression`; `ensure_open_pr` устраняет лишь
|
||||
**ложный** HOLD «no open PR», но не маскирует реально невлитый код (тот → HOLD как прежде).
|
||||
- **`launcher._ensure_pr`** рекомендуется делегировать в `ensure_open_pr` (единый код создания PR),
|
||||
сохранив прежний триггер «только developer-путь».
|
||||
- **Условность как ORCH-35/43/58/71:** kill-switch `merge_verify_autocreate_pr_enabled` (дефолт
|
||||
`true`); область — `merge_verify_applies(repo)` (self-hosting / `merge_verify_repos`); non-self —
|
||||
no-op. `False` → поведение ORCH-074 1:1. Идемпотентность из Gitea (наличие открытого PR), **без
|
||||
миграции БД** (restart-safe). `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, `check_deploy_status`,
|
||||
exit-коды хука, merge-gate, image-freshness — без изменений; `main` не push/force-push.
|
||||
|
||||
Подробнее: [adr-0016](adr/adr-0016-ensure-open-pr-before-merge-verify.md) (amends 0013/0014);
|
||||
детально — `docs/work-items/ORCH-082/06-adr/ADR-001-ensure-open-pr-before-merge-verify.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`.
|
||||
|
||||
### Security-гейт: secret-scanning + dependency audit перед мержем (ORCH-022 — реализовано)
|
||||
Автономный конвейер вливал ветку в `main` без проверки на утёкший секрет (ключ/токен/пароль/
|
||||
приватный ключ) и уязвимую зависимость (CVE); для self-hosting один секрет/CVE через одну
|
||||
задачу уезжал в общий прод всех проектов (CLAUDE.md §8). ORCH-022 вводит детерминированный
|
||||
(без LLM) **security-гейт как под-гейт ребра `deploy-staging → deploy`**, рядом с merge-gate
|
||||
(ORCH-043) и image-freshness (ORCH-058), исполняемый **ПЕРВЫМ** среди edge-под-гейтов
|
||||
(ДО merge-gate). Паттерн соседей: leaf `src/security_gate.py` (never-raise) + тонкая обёртка
|
||||
`check_security_gate` в `QG_CHECKS` + врезка `_handle_security_gate` в `advance_stage`.
|
||||
`STAGE_TRANSITIONS` и схема БД — **без изменений**.
|
||||
- **Secret-scanning (`gitleaks`, offline):** скан `origin/main..HEAD`; любой секрет вне
|
||||
аллоулиста `.gitleaks.toml` → вклад в FAIL. Offline → гарантия «секрет всегда блокирует»
|
||||
не зависит от сети (безусловна).
|
||||
- **Dependency audit (`pip-audit`, OSV/PyPI):** severity ≥ `security_dep_block_severity`
|
||||
(дефолт `HIGH`) → FAIL; ниже / UNKNOWN → warning. Недоступность фида → **fail-open +
|
||||
громкий warning** (анти-петля ORCH-061; флаг `security_dep_audit_fail_closed` для строгого
|
||||
режима). best-effort при доступности фида.
|
||||
- **ПЕРВЫМ, ДО merge-gate:** дёшево фейлить до дорогих rebase/rebuild; скан ветки ДО rebase
|
||||
не «обвиняет» задачу в CVE из обновившегося `main`; до захвата merge-lease → при FAIL lease
|
||||
освобождать не нужно.
|
||||
- **Артефакт `17-security-report.md`** (YAML-frontmatter `security_status`/`secrets_found`/
|
||||
`deps_blocking`/`deps_warning`/`deps_audit_degraded`); вердикт читается ТОЛЬКО из
|
||||
frontmatter (гейт пишет → читает обратно через `parse_security_status` → возвращает: единый
|
||||
источник истины), negative-токен авторитетен, битый/нет → fail-closed.
|
||||
- **FAIL → откат на `development`** + developer-retry (общий `_developer_retry_count`, cap 3,
|
||||
затем `set_issue_blocked` + Telegram); `task_desc` несёт дословные находки (ORCH-046).
|
||||
- **Условность как ORCH-35/43/58:** `security_gate_enabled` + `security_gate_repos` (пусто →
|
||||
только self-hosting); never-raise; таймаут `security_scan_timeout_s`; гейт не деплоит/не
|
||||
рестартит прод. v1 — Python-only; SAST/мульти-стек — follow-up (BR-14).
|
||||
|
||||
Подробнее: [adr-0012](adr/adr-0012-security-gate.md), детально —
|
||||
`docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md`.
|
||||
|
||||
### Live-трекер: зачистка сирот + эффорт в карточке + честное время (ORCH-087 — реализовано)
|
||||
Скалярный `tasks.tracker_message_id` (только последний `message_id`) при рассинхроне
|
||||
bump-режима (доминанты: гонка двух `update_task_tracker` и delete-fail+send-ok)
|
||||
терял ссылку на прежние карточки → **осиротевшие «замёрзшие»** карточки (скриншот
|
||||
ORCH-082: `📍 To Analyse` на задаче, реально дошедшей до `deploy`). G0-расследование
|
||||
([ADR-001](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md)):
|
||||
рендер исправен, корень — потеря учёта старых mid. Решение (bump сохраняется как
|
||||
дефолт — фича «карточка внизу» ORCH-042/067):
|
||||
- **G1 — полный учёт mid:** аддитивная таблица-леджер `tracker_messages(task_id,
|
||||
message_id, created_at, deleted_at)` (вариант A1; JSON-массив A2 отклонён —
|
||||
lost-update при гонке). На каждом bump зачищаются ВСЕ незакрытые mid (`deleted_at
|
||||
IS NULL`): успех/«already gone» → `deleted_at`, transient → остаётся для ретрая;
|
||||
новый mid в леджер + `set_tracker_message_id` ТОЛЬКО при `send is not None` (BR-6).
|
||||
Скаляр `tracker_message_id` сохранён (BC). Остаточная гонка самозалечивается за один
|
||||
переход (лок не вводится). Known-limitation: Telegram 48ч (сироты старше неудаляемы).
|
||||
- **G2/G3 — заголовок/deploy-цикл:** после G1 единственная живая карточка несёт
|
||||
заголовок текущей стадии; `_LIVE_BRANCH_LABELS` дополняется ключом `confirm_deploy`
|
||||
(полнота цикла `Awaiting Deploy → Deploying → Confirm Deploy → Monitoring → Done`).
|
||||
- **BR-EFF — эффорт в строке стадии:** новая колонка `agent_runs.effort TEXT`,
|
||||
стамп фактического `resolve_agent_effort` в `launcher._spawn` (CLI эффорт не
|
||||
возвращает); рендер `· {model} · {effort}` (developer=`xhigh`, tester/deployer=
|
||||
`medium`, прочие=`high`); пустой → суффикс опускается.
|
||||
- **BR-G5 — честное время:** done-строка `⏱️ Агенты {agent} · твоё {review~cap} ·
|
||||
общее с ожиданием {wall}` — три независимых подписанных метрики; `agent`=Σ
|
||||
`agent_runs` (главная, точная); «твоё» ограничено порогом
|
||||
`tracker_brd_review_cap_s` (дефолт 2ч, маркер `~` при отсечке аномального застоя);
|
||||
`wall` подписан «с ожиданием», не выдаётся за сумму.
|
||||
- **Инварианты:** `STAGE_TRANSITIONS`/`QG_CHECKS`/стадии — без изменений; миграции
|
||||
аддитивны/идемпотентны (общая прод-БД, enduro не трогается); never-raise,
|
||||
`disable_notification`, `plane_issue_link` (ORCH-067), `disable_web_page_preview`
|
||||
(ORCH-080) — сохранены; разработка поверх свежего `origin/main` (ORCH-86),
|
||||
`reconciler.py` не эродируется.
|
||||
|
||||
Детально — [ADR-001](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md),
|
||||
`docs/work-items/ORCH-087/08-data-requirements.md`.
|
||||
|
||||
### Reconciler: реконсиляция потерянных webhook (ORCH-053 — реализовано)
|
||||
Конвейер продвигается только входящими webhook; потерянное событие (502 на ребилде,
|
||||
нет ретраев у Plane/Gitea, неразрезолвленный `sha→branch`) → задача застревает молча
|
||||
(инцидент ORCH-044). Фоновый поток `reconciler` периодически (`reconcile_interval_s`)
|
||||
находит застрявшие задачи и доигрывает пропущенный переход **через те же штатные
|
||||
гейты/обработчики**, что и webhook:
|
||||
- **F-1 gate-side:** для задач со `stage∉{done}`, без активного job и
|
||||
`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).
|
||||
**ORCH-086 (закрытие F-1-пробела ORCH-068):** терминал-исключение и `state_uuid`-dedup
|
||||
(изначально только F-2) распространены на F-1. После дешёвых локальных гардов F-1 делает
|
||||
**один** резолв Plane-статуса задачи на тик (общий fetch для Guard 2 + терминал-скипа +
|
||||
`_note_unblock`); терминальная задача (группа Plane `completed`/`cancelled`, fallback —
|
||||
логические ключи `done`/`cancelled`, ЛИБО стадия в БД орка ∈ `{done, cancelled}`) →
|
||||
**безусловный** ранний скип (`skipped_terminal_total++`, без `advance`/уведомления; не подчинён
|
||||
`reconcile_skip_blocked_enabled`). Вызов `_note_unblock` на F-1 теперь передаёт `state_uuid` →
|
||||
in-memory dedup работает на обоих путях (страховка от повтора после рестарта). Лечит
|
||||
периодическое ложное «ET-002 done разблокирована (потерян webhook)» для терминальных в Plane
|
||||
задач (enduro/orchestrator), сохраняя легитимный unblock реально застрявшей не-терминальной
|
||||
задачи. `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД/сигнатуры/новые флаги — без изменений. Детали —
|
||||
`docs/work-items/ORCH-086/06-adr/ADR-001-reconciler-f1-terminal-skip-and-dedup.md`.
|
||||
- **F-2 plane-side:** опрос Plane API per-project → `handle_status_start` /
|
||||
`handle_verdict` из `webhooks/plane.py` (логика не дублируется).
|
||||
**ORCH-068 (livelock-fix):** (1) задачи в **терминальной группе** Plane
|
||||
(`state.group ∈ {completed, cancelled}`, fallback — логические ключи
|
||||
`done`/`cancelled`) исключаются из actionable-выборки per-issue — проектно-независимо,
|
||||
устойчиво к UUID-алиасингу после переименований статусов (ORCH-066); (2) `_note_unblock`
|
||||
(лог + Telegram + `unblocked_total`) вызывается ТОЛЬКО при **подтверждённом state change**
|
||||
(сравнение стадии задачи до/после `_dispatch`; no-op dispatch → тишина), плюс in-memory
|
||||
дедуп по `issue_id→state`. Восстанавливает инвариант silence-when-in-sync (AC-9/AC-10).
|
||||
Детали — `docs/work-items/ORCH-068/06-adr/ADR-001-reconciler-terminal-exclusion-and-cache-ttl.md`.
|
||||
- **F-3:** усиление `sha→branch` в `handle_ci_status` (БД-fallback по единственной
|
||||
development-задаче repo; неоднозначность → не резолвим).
|
||||
- **F-4 observability:** при разблокировке — лог-строка `reconciler: <wi> <stage>
|
||||
разблокирована (потерян webhook)` + Telegram (`reconcile_notify_unblock`); снимок
|
||||
состояния в `GET /queue` (блок `reconcile`). **ORCH-068** добавляет в снимок
|
||||
счётчики `skipped_terminal_total` (исключённые терминалы) и `deduped_total`
|
||||
(подавленные повторные нотификации).
|
||||
|
||||
Реализация: `src/reconciler.py` (daemon-поток по образцу `queue_worker`), стартует в
|
||||
`main.lifespan` **после** `worker.start()`, останавливается в `finally` **перед**
|
||||
`worker.stop()`.
|
||||
|
||||
Инварианты: источник истины — гейт/Plane, не событие; идемпотентность (active-job
|
||||
guard + atomic-claim на создании под process-wide Lock + grace + `max_concurrency=1`);
|
||||
never-raise на единицу работы; тишина при синхронности; restart-safe; kill-switch
|
||||
`ORCH_RECONCILE_ENABLED` (+ `ORCH_RECONCILE_PLANE_ENABLED` гасит только F-2). Схема БД
|
||||
и реестры (`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.
|
||||
- Deploy / deploy-staging FAILED → откат на `development`.
|
||||
- Merge-gate FAIL (конфликт rebase / красный re-test, ORCH-043) → откат на `development` + retry; `merge-lock busy` → **defer** (не откат, dev-retry не тратится).
|
||||
- `get_previous_stage` использует порядок ключей `STAGE_TRANSITIONS`.
|
||||
|
||||
### Обогащение `task_desc` при заворотах (ORCH-046)
|
||||
@@ -73,7 +597,9 @@ created → analysis → architecture → development → review → testing →
|
||||
- `events` — входящие вебхуки (дедуп)
|
||||
- `tasks` — задачи и их стадии
|
||||
- `agent_runs` — запуски агентов (run_id, usage, cost)
|
||||
- `jobs` — очередь задач (ORCH-1)
|
||||
- `jobs` — очередь задач (ORCH-1); колонка `pid` (ORCH-065) — pid агентского процесса для liveness-детекции зомби job-reaper'ом
|
||||
- `job_deps` — декларативные зависимости задач (ORCH-026, Уровень B): `(task_id, depends_on_task_id)`, аддитивная; источник истины планировщика для гейта «B ждёт A»
|
||||
- `repo_freeze` — durable per-repo rollback-freeze (ORCH-088, FR-5): `(id, repo, frozen_at, reason, work_item_id, cleared_at)`, аддитивная append-only; активный freeze ⇔ строка репо с `cleared_at IS NULL`. Выставляется post-deploy `DEGRADED` (`set_repo_freeze`), снимается вручную (`POST /serial-gate/unfreeze` → `cleared_at=now`). Гейтит serial-claim безусловно (деградировавшая задача уже `done`)
|
||||
|
||||
## Изоляция (git worktree, ORCH-2)
|
||||
Каждая задача исполняется в отдельном git worktree, ветки не пересекаются. Репозитории проектов разделены под `/repos/<project>`.
|
||||
@@ -83,7 +609,8 @@ created → analysis → architecture → development → review → testing →
|
||||
|--------|------|----------|
|
||||
| GET | `/health` | health check |
|
||||
| GET | `/status` | активные задачи (stage != done) |
|
||||
| GET | `/queue` | очередь: counts + max_concurrency + последние jobs |
|
||||
| GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + reaper (ORCH-065) + post_deploy (ORCH-021) + task_deps (ORCH-026) + serial_gate (ORCH-088) + последние jobs |
|
||||
| POST | `/serial-gate/unfreeze` | ORCH-088 (FR-5): ручное снятие per-repo rollback-freeze (query/body `repo=<repo>`) → `{ok, repo, cleared, frozen}`; идемпотентно. Альтернатива — `UPDATE repo_freeze SET cleared_at=datetime('now') WHERE repo=? AND cleared_at IS NULL` |
|
||||
| POST | `/webhook/plane` | Plane webhook |
|
||||
| POST | `/webhook/gitea` | Gitea webhook (push, PR, CI status) |
|
||||
|
||||
@@ -97,4 +624,8 @@ created → analysis → architecture → development → review → testing →
|
||||
Схема БД, потоки данных, resilience-слой, детали Dockerfile — [internals.md](internals.md).
|
||||
|
||||
---
|
||||
*Актуально на 2026-06-05 (main `f1b3146`). Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.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-022 (security-гейт: secret-scanning gitleaks + dependency audit pip-audit как под-гейт ребра `deploy-staging → deploy` ПЕРВЫМ, adr-0012, `docs/work-items/ORCH-022/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-022-security-secret-scanning (leaf src/security_gate.py never-raise + check_security_gate в src/qg/checks.py `QG_CHECKS` + врезка _handle_security_gate в src/stage_engine.py блок `current_stage == "deploy-staging"` ПЕРВОЙ; флаги `security_*` в src/config.py; gitleaks (pinned) в Dockerfile, pip-audit в requirements.txt, `.gitleaks.toml` в корне; артефакт 17-security-report.md; обновлять также при изменении этих мест).*
|
||||
*Актуально на 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-059 (выделенный статус-триггер прод-деплоя «Confirm Deploy», ADR `docs/work-items/ORCH-059/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-059 (маппинг `"Confirm Deploy"→"confirm_deploy"` в src/plane_sync.py `_PLANE_NAME_TO_KEY`, НЕ в `_DEFAULT_STATES` = fail-closed; ветка `handle_confirm_deploy` + fail-closed `.get("confirm_deploy")` в src/webhooks/plane.py `handle_issue_updated`; keyword-only `confirm_deploy` в src/stage_engine.py `advance_stage` — Фаза B деплоит ТОЛЬКО при `confirm_deploy=True`, иначе `Approved`-на-`deploy` = no-op; CTA Фазы A просит «Confirm Deploy»; эксплуатация — статус доски «Confirm Deploy» в Plane-проекте ORCH, `docs/work-items/ORCH-059/07-infra-requirements.md`).*
|
||||
*Актуально на 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`; обновлять при изменении этих мест).*
|
||||
*Актуально на 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-068 (livelock-fix reconciler F-2: терминал-исключение по группе состояния + `_note_unblock` только при подтверждённом state change + дедуп; TTL `_STATES_CACHE`, `docs/work-items/ORCH-068/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-068 (D1 терминал-гард по группе `_is_terminal_state` + `get_project_state_groups` в src/plane_sync.py; D2 сравнение стадии до/после `_dispatch` + дедуп-словарь в src/reconciler.py; TTL-запись `_STATES_CACHE` + флаг `plane_states_ttl_s` в src/config.py; счётчики `skipped_terminal_total`/`deduped_total` в `/queue`; обновлять также при изменении src/reconciler.py F-2, src/plane_sync.py `get_project_states`/`get_project_state_groups`/`_STATES_CACHE`).*
|
||||
*Актуально на 2026-06-09. Статус доработки: ORCH-088 (per-repo serial gate, Этап 1 serial e2e, adr-0017, `docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`) — реализовано в ветке feature/ORCH-088 (leaf src/serial_gate.py never-raise: gate-фрагмент в src/db.py `claim_next_job` fail-OPEN c FIFO-условием `t2.id < jobs.task_id` + freeze `repo_freeze.cleared_at IS NULL`, freeze-решения fail-CLOSED; отложенный срез ветки src/webhooks/plane.py `start_pipeline` → src/agents/launcher.py `_materialize_deferred_branch` (sync `asyncio.run` в worker-потоке) при claim analyst-job; durable freeze таблица `repo_freeze` (idempotent миграция в init_db) + `set_repo_freeze` в src/stage_engine.py DEGRADED-ветке `run_post_deploy_monitor` + ручное снятие `POST /serial-gate/unfreeze` в src/main.py; флаги `serial_gate_enabled`/`serial_gate_repos`/`serial_gate_freeze_enabled` в src/config.py; блок `serial_gate` в `GET /queue`; `STAGE_TRANSITIONS`/`QG_CHECKS` НЕ трогаются; обновлять также при изменении этих мест).*
|
||||
|
||||
@@ -9,6 +9,27 @@ Per-work-item решения живут в `docs/work-items/<id>/06-adr/ADR-NNN-
|
||||
| adr-0002 | Очередь задач вместо in-process потоков | accepted | 2026-06-03 | ORCH-1 |
|
||||
| adr-0003 | Условный staging-гейт перед прод-деплоем | accepted | 2026-06-05 | ORCH-35 |
|
||||
| adr-0004 | Поллинг с ретраем в check_ci_green (фикс CI-race) | accepted | 2026-06-05 | ORCH-045 |
|
||||
| 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 |
|
||||
| adr-0012 | Security-гейт (secrets/deps) | accepted | 2026-06-08 | ORCH-022 |
|
||||
| adr-0013 | Merge-в-main + пост-деплой верификация как условие `done` | accepted | 2026-06-08 | ORCH-071 |
|
||||
| adr-0014 | SHA-в-main — единственный критерий merge-verify + регресс-гард | accepted | 2026-06-08 | ORCH-073 |
|
||||
| adr-0015 | Зависимости задач (B ждёт A) + сериализация merge внутри репо | accepted | 2026-06-08 | ORCH-026 |
|
||||
| adr-0016 | ensure_open_pr — гарантированный код-PR перед merge-verify | accepted | 2026-06-09 | ORCH-082 |
|
||||
| adr-0017 | Per-repo serial gate (пакетный автономный режим, serial e2e) | proposed | 2026-06-09 | ORCH-088 |
|
||||
|
||||
> ⚠️ Историческая коллизия: номер `0007` занят двумя файлами —
|
||||
> `adr-0007-reconciler.md` (ORCH-053) и `adr-0007-executable-self-deploy.md`
|
||||
> (ORCH-036). Оба accepted; для новых сквозных ADR использовать следующий
|
||||
> свободный номер (текущий максимум — `0017`).
|
||||
> adr-0014 **amends** adr-0013 (меняет критерий merge-verify на «SHA-в-main»).
|
||||
> adr-0016 **amends** adr-0013/0014 (гарантирует открытый код-PR перед merge_pr, ORCH-082).
|
||||
|
||||
## Формат
|
||||
**Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded.
|
||||
|
||||
42
docs/architecture/adr/adr-0005-container-runs-as-host-uid.md
Normal file
42
docs/architecture/adr/adr-0005-container-runs-as-host-uid.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# adr-0005: Контейнеры оркестратора бегут под uid:gid хоста (1000:1000)
|
||||
|
||||
- **Статус:** accepted
|
||||
- **Дата:** 2026-06-06
|
||||
- **Задача:** ORCH-040
|
||||
|
||||
## Контекст
|
||||
Оба контейнера (`orchestrator`, `orchestrator-staging`) запускались под `uid=0 (root)` и
|
||||
монтировали хостовый `/home/slin/repos` → `/repos` (rw). Claude-CLI агенты исполняются
|
||||
`subprocess.Popen` внутри контейнера под тем же root, поэтому все артефакты конвейера
|
||||
(git worktree, коммиты в `docs/`) появлялись на хосте как `root:root`. Деплой прода под
|
||||
`slin` (uid 1000) ломался на правах git до ручного `chown`. Это сквозное свойство рантайма:
|
||||
касается агентов **всех** проектов, а не отдельной фичи.
|
||||
|
||||
## Решение
|
||||
Оба сервиса в `docker-compose.yml` запускаются под `user: "1000:1000"` (uid:gid хоста `slin`).
|
||||
- `group_add: ["999"]` сохраняется — доступ к docker.sock идёт через gid 999, не через root.
|
||||
- target SSH-маунта приведён к `/home/slin/.ssh` (был `/root/.ssh`), синхронно с
|
||||
`HOME=/home/slin`, который форсит launcher → единый HOME по осям uid/claude/ssh.
|
||||
- Образ и launcher не меняются: numeric uid не требует записи в `/etc/passwd`,
|
||||
`git config --system safe.directory '*'` уже есть.
|
||||
|
||||
Обязательные host-prerequisites (Owner, вне кода): доступ uid 1000 к
|
||||
`/home/slin/.claude/.credentials.json` (блокер), ssh-ключи в новом HOME, рестарт prod
|
||||
только в окно тишины. Детали и команды — work-item ADR-001 и `docs/operations/INFRA.md`.
|
||||
|
||||
## Альтернативы
|
||||
- **drop-privileges только для subprocess агента** (`gosu`/`setuid`) — контейнер остаётся
|
||||
root; новый код в горячем пути launcher, два uid в одном контейнере; отклонён.
|
||||
- **chown-хук после каждой стадии** — лечит симптом, требует root внутри контейнера
|
||||
(несовместимо), хрупкий пост-шаг; отклонён (fallback на крайний случай).
|
||||
|
||||
## Последствия
|
||||
- Артефакты создаются под `slin:slin`; деплой прода не требует ручного `chown`.
|
||||
- HOME консистентен (uid = claude = ssh = `/home/slin`); устранён рассинхрон SSH-маунта.
|
||||
- Появляется явная привязка рантайма к uid 1000 хоста (задокументирована в INFRA.md).
|
||||
- Прод-рестарт self = групповой риск (общий инстанс с enduro-trails) → строго окно тишины;
|
||||
страховка — staging-гейт (adr-0003).
|
||||
|
||||
## Связи
|
||||
adr-0003 (staging-гейт — обязательная проверка перед прод-рестартом self),
|
||||
adr-0001 (`is_self_hosting_repo`), work-item `docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md`.
|
||||
53
docs/architecture/adr/adr-0006-merge-gate.md
Normal file
53
docs/architecture/adr/adr-0006-merge-gate.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# adr-0006: Merge-gate — догон `main` + re-test + сериализация слияний
|
||||
|
||||
- **Статус:** proposed
|
||||
- **Дата:** 2026-06-06
|
||||
- **Задача:** ORCH-043
|
||||
- **Детальный ADR:** `docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md`
|
||||
|
||||
## Контекст
|
||||
Ветка валидируется относительно того `main`, из которого создана, а не относительно `main`
|
||||
на момент слияния. Параллельная задача могла влиться раньше → **семантический конфликт
|
||||
слияния** (git мержит без текстового конфликта, но `main` сломан). Для self-hosting это
|
||||
красный `main` инструмента, обслуживающего все проекты. Слияние в `main` делает
|
||||
deployer-агент в начале стадии `deploy`; замена механизма PR-merge — вне объёма.
|
||||
|
||||
## Решение
|
||||
Детерминированный merge-gate (`check_branch_mergeable`, без LLM) на ребре
|
||||
`deploy-staging → deploy`, ДО запуска deployer'а, который мержит. `STAGE_TRANSITIONS` не
|
||||
меняется (минимальный blast-radius); в `QG_CHECKS` добавлен `check_branch_mergeable`.
|
||||
|
||||
- **Догон:** ветка отстаёт ⇔ `origin/main` не предок HEAD → `rebase origin/main` в worktree
|
||||
+ `push --force-with-lease` (ТОЛЬКО ветка задачи; `main` — никогда). Текстовый конфликт →
|
||||
`rebase --abort` → откат на `development`.
|
||||
- **Re-test:** `python -m pytest tests/` в worktree догнанной ветки, тайм-аут
|
||||
`merge_retest_timeout_s`. Красный/тайм-аут → откат на `development`.
|
||||
- **Сериализация (BR-5):** файловый **merge-lease** на репо
|
||||
(`<repos_dir>/.merge-lease-<repo>.json`), живёт от гейта до фактического merge.
|
||||
Acquire **неблокирующий** (anti-deadlock при `max_concurrency=1`): busy → **defer**
|
||||
(re-enqueue deployer с задержкой через `available_at`), не rollback. Release — на
|
||||
PR-merged вебхуке / `deploy→done` / откате / по возрасту (crash-реклейм). Restart-safe.
|
||||
- **Условность (как ORCH-35):** реален для `orchestrator`; прочие репо — no-op. Флаги
|
||||
`merge_gate_enabled` / `merge_gate_repos` для поэтапного раската.
|
||||
|
||||
## Альтернативы
|
||||
- **Новая стадия `merge-gate`** (кандидат B) — «пустая» стадия без агента не имеет триггера
|
||||
(`advance_stage` срабатывает только на завершении агента/вебхуке); потребовала бы chaining
|
||||
в движке (не restart-safe) или синтетический job-тип. Отклонено.
|
||||
- **Перенос merge в детерминированный шаг оркестратора** (кандидат C) — запрещён объёмом
|
||||
(замена механизма PR-merge вне scope). Отклонено.
|
||||
- **Блокирующий lock** — дедлок при одном worker-слоте. Отклонено в пользу defer.
|
||||
|
||||
## Последствия
|
||||
- Сценарий «две зелёные ветки ломают `main`» закрыт: re-test против актуального `main` +
|
||||
сериализация слияний.
|
||||
- Плата: merge-gate — «скрытый» под-гейт ребра (нет в `STAGE_TRANSITIONS`); сериализация
|
||||
опирается на PR-merged вебхук со страховкой реклеймом по возрасту; defer перепрогоняет
|
||||
staging; длинный re-test держит worker-слот.
|
||||
- Сквозное изменение конвейера → `arch:major-change`; прод-деплой ORCH-043 строго через
|
||||
staging-гейт (8501).
|
||||
|
||||
## Связи
|
||||
adr-0001 (`is_self_hosting_repo`), adr-0003 (условный staging-гейт — образец условности),
|
||||
adr-0002 (очередь / `available_at` для defer), ORCH-2 (worktree-изоляция), ORCH-046
|
||||
(дословный reason в `task_desc` при откате).
|
||||
64
docs/architecture/adr/adr-0007-executable-self-deploy.md
Normal file
64
docs/architecture/adr/adr-0007-executable-self-deploy.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# ADR-0007: Исполняемый самодеплой стадии `deploy` (Вариант B, ORCH-36)
|
||||
|
||||
## Статус
|
||||
Accepted (design) — реализация в ветке `feature/ORCH-036`.
|
||||
|
||||
## Контекст
|
||||
Стадия `deploy` была «бумажной»: deployer-агент писал `deploy_status:` в
|
||||
`14-deploy-log.md`, гейт `check_deploy_status` парсил вердикт и двигал
|
||||
`deploy → done`. Реального деплоя не было. ORCH-36 делает стадию исполняемой для
|
||||
self-hosting (`orchestrator`), сохраняя прежний ssh-путь для остальных репо.
|
||||
|
||||
Три ограничения формируют дизайн (детально — `docs/work-items/ORCH-036/06-adr/ADR-001`):
|
||||
1. **Self-restart**: рестарт прод-контейнера 8500 убивает in-container процесс →
|
||||
рестарт делает ВНЕШНИЙ host-процесс.
|
||||
2. **Status-only verdict model**: approve = смена статуса Plane на `Approved`
|
||||
(комментарии не управляют конвейером).
|
||||
3. **Гонка гейта**: вердикт нельзя читать до завершения асинхронного хука.
|
||||
|
||||
## Решение
|
||||
Для self-hosting стадия `deploy` исполняется в три фазы детерминированным кодом
|
||||
(без LLM в критическом пути self-restart):
|
||||
|
||||
- **Фаза A (вход в `deploy`)** — для self + `deploy_require_manual_approve=true`
|
||||
вместо запуска прод-deployer выставляется approval-pending статус Plane + запрос
|
||||
approve (Plane-коммент + Telegram). Перехват в `advance_stage` на ребре
|
||||
`deploy-staging → deploy` (после `check_staging_status` и merge-gate).
|
||||
- **Фаза B (Plane → Approved)** — `advance_stage(deploy, finished_agent=None)`
|
||||
запускает **detached host-процесс** (ssh + setsid → `orchestrator-deploy-hook.sh`
|
||||
с прод-параметрами и build-once retag) и ставит **детерминированный finalizer-job**
|
||||
с задержкой; маркер `initiated` — идемпотентность. Возврат БЕЗ advance.
|
||||
- **Фаза C (finalizer)** — после рестарта новый контейнер дочитывает sentinel
|
||||
`result` (exit-code хука), маппит `0→SUCCESS / иначе→FAILED`, пишет
|
||||
`14-deploy-log.md`, вызывает `advance_stage(deploy, finished_agent="deployer")`
|
||||
→ существующие контракты: `SUCCESS → done`, `FAILED → откат БАГ-8 на development`.
|
||||
|
||||
### Ключевые инварианты (НЕ меняются)
|
||||
`STAGE_TRANSITIONS`, реестр QG, `check_deploy_status` / `_parse_deploy_status`
|
||||
(frontmatter only), откат БАГ-8, terminal-sync `deploy → done`, merge-gate (ORCH-43),
|
||||
exit-code-контракт хука (0/1/2).
|
||||
|
||||
### Новое (сквозное)
|
||||
- **Детерминированный job-kind** `deploy-finalizer` в очереди (reserved-agent, не
|
||||
LLM): read-result | defer | map+write+advance. Зеркалит детерминизм merge-gate.
|
||||
- **Approve-флаг** `deploy_require_manual_approve` (дефолт `true`; полный авто —
|
||||
отдельная задача после набора метрик доверия, ORCH-54).
|
||||
- **Build-once**: опциональный `SOURCE_IMAGE` retag в хуке (обратно совместимо).
|
||||
- **Restart-safe состояние** деплоя — sentinel-файлы под
|
||||
`<repos_dir>/.deploy-state-<repo>/<wi>/` (как merge-lease), БЕЗ миграции БД.
|
||||
|
||||
### Условность
|
||||
Вся логика — только для `is_self_hosting_repo(repo)` (как ORCH-35). Прочие репо
|
||||
деплоятся прежним синхронным ssh-путём агентом.
|
||||
|
||||
## Последствия
|
||||
- `deploy_status: SUCCESS` доказан реальным health-ok; критический путь self-restart
|
||||
детерминирован.
|
||||
- Вводится новая под-компонента (finalizer job-handler) → изменение помечено
|
||||
`arch:major-change`.
|
||||
- Approve вписан в status-only модель: restart-safe, аудируемо, идемпотентно.
|
||||
- На старте — обязательный ручной approve; молчаливых деплоев нет (Plane+Telegram).
|
||||
|
||||
## Связанные ADR
|
||||
`adr-0003` (staging-gate), `adr-0006` (merge-gate), `adr-0005` (run-as-host-uid).
|
||||
Детальный per-work-item: `docs/work-items/ORCH-036/06-adr/ADR-001-executable-self-deploy.md`.
|
||||
86
docs/architecture/adr/adr-0007-reconciler.md
Normal file
86
docs/architecture/adr/adr-0007-reconciler.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# adr-0007: Reconciler застрявших стадий (sweeper потерянных webhook)
|
||||
|
||||
- **Статус:** accepted (реализовано в `src/reconciler.py`)
|
||||
- **Дата:** 2026-06-06
|
||||
- **Задача:** ORCH-053
|
||||
- **Детальный ADR:** `docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md`
|
||||
|
||||
## Контекст
|
||||
Конвейер продвигается **только** входящими webhook (Plane status / Gitea CI/PR).
|
||||
Потерянное событие (502 на ребилде, отсутствие ретраев у Plane/Gitea,
|
||||
неразрезолвленный `sha→branch`) → источник истины изменился, а стадия задачи —
|
||||
нет; задача застревает молча (инцидент ORCH-044). Существующий resilience
|
||||
(`requeue_running_jobs`, orphan-recovery, events de-dup ORCH-5, `ci_poll`
|
||||
ORCH-045) работает на уровне jobs/agent_runs и **не реконсилирует**
|
||||
рассинхрон «источник истины ≠ стадия задачи».
|
||||
|
||||
## Решение
|
||||
Фоновый daemon-поток `src/reconciler.py` (паттерн `queue_worker`, module-singleton,
|
||||
`threading.Event`), стартует в `main.lifespan` после `worker.start()`, стоп в
|
||||
`finally` перед `worker.stop()`. Две взаимодополняющие ветки на каждом тике
|
||||
(`reconcile_interval_s`, дефолт 120с):
|
||||
|
||||
- **F-1 gate-side** (локальная БД): для каждой `task` где `stage∉{done}`, **нет**
|
||||
активного job, `age(updated_at) ≥ grace_for_stage(stage)` — read-only пред-оценка
|
||||
канонического QG стадии; если зелёный → продвижение **штатным**
|
||||
`stage_engine.advance_stage(..., finished_agent=None)` (тот же путь, что у Plane
|
||||
Approved-webhook). Красный → **тишина** (нет advance, нет нотификаций — спам
|
||||
структурно невозможен). `analysis` F-1 **не** реконсилирует (человеческий гейт →
|
||||
отдан F-2).
|
||||
- **F-2 plane-side** (опрос Plane API per-project через `list_issues_by_state`):
|
||||
`In Progress`+нет задачи → `handle_status_start`; `Approved`+не сдвинута →
|
||||
`handle_verdict(approved=True)`; `Rejected`+не откатана →
|
||||
`handle_verdict(approved=False)`. Обработчики `webhooks/plane.py`
|
||||
**переиспользуются** (async → `asyncio.run` из sync-потока), логика не дублируется.
|
||||
- **F-3:** усиление `sha→branch` в `handle_ci_status` (БД-fallback по
|
||||
`repo`+`stage='development'`, видимость на INFO) — defense-in-depth.
|
||||
|
||||
**Инварианты:** источник истины — гейт/Plane, не событие; продвижение только через
|
||||
`advance_stage`; идемпотентность (active-job guard + atomic-claim на создании +
|
||||
grace + `max_concurrency=1`); never-raise на единицу работы; тишина при
|
||||
синхронности; restart-safe; kill-switch.
|
||||
|
||||
## Альтернативы
|
||||
- **Флаг подавления нотификаций в `advance_stage`** — отклонён: меняет общий
|
||||
критический путь. Вместо этого «не вызывать advance_stage на красном гейте».
|
||||
- **UNIQUE-индекс `tasks.plane_id`** для анти-дубля — отклонён как primary: риск
|
||||
падения миграции на проде; выбран process-wide `threading.Lock` (single-process
|
||||
топология). Индекс — задокументированное будущее упрочнение для multi-process.
|
||||
- **Отдельная стадия/QG реконсиляции** — вне объёма; нарушает «источник истины —
|
||||
существующий гейт».
|
||||
- **Реконсиляция analysis по локальным артефактам** — отклонена: автопродвижение
|
||||
неодобренного человеком BRD.
|
||||
|
||||
## Последствия
|
||||
- Потерянный webhook ≠ молча застрявшая задача; ручной heartbeat-watchdog не нужен;
|
||||
резервная сетка к ORCH-51 (буфер недоставленных) и ORCH-36 (deploy).
|
||||
- Плата: фоновый поток + опрос Plane API (митигируется интервалом/фильтром/
|
||||
per-project); двойная оценка гейта на зелёной задаче; анти-дубль опирается на
|
||||
single-process-допущение (как и очередь ORCH-1).
|
||||
- Self-hosting: `reconcile_enabled` — обязательный kill-switch; поэтапный раскат
|
||||
(`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,
|
||||
тишина при пропуске).
|
||||
|
||||
- **ORCH-068** (`docs/work-items/ORCH-068/06-adr/ADR-001-reconciler-terminal-exclusion-and-cache-ttl.md`):
|
||||
фикс livelock F-2 (спам `_note_unblock` по синхронизированной done-задаче после
|
||||
ORCH-066). F-2 исключает терминалы по **группе состояния** (`completed`/`cancelled`,
|
||||
fallback — ключи `done`/`cancelled`) проектно-независимо; `_note_unblock` — только при
|
||||
подтверждённом state change (сравнение стадии до/после `_dispatch`) + in-memory дедуп;
|
||||
`_STATES_CACHE` получает TTL (`ORCH_PLANE_STATES_TTL_S`, дефолт 300с, `0`=lifetime).
|
||||
Инварианты adr-0007 сохранены (источник истины — Plane; реестры/схема/`handle_*`/F-1/F-3
|
||||
не меняются; never-raise; kill-switch'и).
|
||||
|
||||
## Связи
|
||||
adr-0002 (очередь / `available_at`, single-process-singleton), adr-0003 (условный
|
||||
гейт — образец условности/флагов раската), adr-0006 (merge-gate как под-гейт ребра
|
||||
внутри `advance_stage`), adr-0001 (реестр проектов для F-2 per-project), ORCH-5
|
||||
(events de-dup — защита от дублей; reconciler — обратная защита от потерь),
|
||||
ORCH-045 (`ci_poll`).
|
||||
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.
|
||||
63
docs/architecture/adr/adr-0012-security-gate.md
Normal file
63
docs/architecture/adr/adr-0012-security-gate.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# adr-0012: Security-гейт — secret-scanning + dependency audit перед мержем
|
||||
|
||||
- **Статус:** proposed
|
||||
- **Дата:** 2026-06-07
|
||||
- **Задача:** ORCH-022
|
||||
- **Детальный ADR:** `docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md`
|
||||
|
||||
## Контекст
|
||||
Оркестратор автономен: `developer` пишет код без человека-фильтра. Перед слиянием ветки в
|
||||
`main` нет проверки на утёкший секрет (ключ/токен/пароль/приватный ключ) и уязвимую
|
||||
зависимость (CVE). Для self-hosting один общий прод-инстанс обслуживает все проекты с общей
|
||||
БД — секрет/CVE через одну задачу попадает в прод всех (CLAUDE.md §self-hosting, §8). Фактический
|
||||
мерж PR в `main` делает `deployer` в начале стадии `deploy`.
|
||||
|
||||
## Решение
|
||||
Детерминированный (без LLM) **security-гейт как под-гейт ребра `deploy-staging → deploy`**,
|
||||
рядом с merge-gate (ORCH-043) и image-freshness (ORCH-058), исполняемый **ПЕРВЫМ** среди
|
||||
edge-под-гейтов (ДО merge-gate). `STAGE_TRANSITIONS` не меняется; в `QG_CHECKS` добавлен
|
||||
`check_security_gate`. Паттерн — как у соседей: leaf-модуль `src/security_gate.py`
|
||||
(never-raise) + тонкая обёртка в `QG_CHECKS` + врезка `_handle_security_gate` в `advance_stage`.
|
||||
|
||||
- **Secret-scanning (`gitleaks`, offline):** скан `origin/main..HEAD`; любой секрет вне
|
||||
аллоулиста (`.gitleaks.toml`) → вклад в FAIL. Offline → гарантия «секрет всегда блокирует»
|
||||
не зависит от сети.
|
||||
- **Dependency audit (`pip-audit`, OSV/PyPI):** severity ≥ `security_dep_block_severity`
|
||||
(дефолт `HIGH`) → FAIL; ниже / UNKNOWN → warning. Недоступность фида → **fail-open +
|
||||
громкий warning** (анти-петля; флаг `security_dep_audit_fail_closed` для строгого режима).
|
||||
- **ПЕРВЫМ на ребре, ДО merge-gate:** дёшево фейлить до дорогих rebase/rebuild; скан ветки
|
||||
ДО rebase не «обвиняет» задачу в CVE, притащенной обновившимся `main` (анти-петля
|
||||
ORCH-061); до захвата merge-lease → при FAIL lease освобождать не нужно.
|
||||
- **Артефакт `17-security-report.md`** с YAML-frontmatter (`security_status`,
|
||||
`secrets_found`, `deps_blocking`, `deps_warning`, `deps_audit_degraded`); вердикт читается
|
||||
ТОЛЬКО из frontmatter (канон), negative-токен авторитетен; битый/нет → fail-closed.
|
||||
- **FAIL → откат на `development`** + developer-retry (общий `_developer_retry_count`, cap 3,
|
||||
затем `set_issue_blocked` + Telegram); `task_desc` несёт дословные находки (ORCH-046).
|
||||
- **Условность (как ORCH-35/43/58):** `security_gate_enabled` + `security_gate_repos`; пусто
|
||||
→ реально только self-hosting (`orchestrator`), прочие репо — no-op pass.
|
||||
- **never-raise**, таймаут `security_scan_timeout_s`, гейт не деплоит/не рестартит прод.
|
||||
|
||||
## Альтернативы
|
||||
- **Вариант R (review-стадия):** diff может разойтись с мержем в `main`; merge-edge — последняя
|
||||
страховка. Отклонено.
|
||||
- **Вариант C (CI-job через `check_ci_green`):** пороги/severity/аллоулист/артефакт плохо
|
||||
выражаются статусом коммита; коуплинг с раннером. Отклонено для v1 (точка расширения).
|
||||
- **Новая стадия `security`:** «пустая» стадия без агента не имеет триггера (как в ORCH-043).
|
||||
Отклонено.
|
||||
- **fail-closed dep-audit / аудит после rebase:** ложные откаты → петля. Отклонено.
|
||||
- **Новая колонка retry в БД:** не нужна (переиспользуем `_developer_retry_count`).
|
||||
|
||||
## Последствия
|
||||
- Класс «тихо влитый секрет/CVE» закрыт: секреты — безусловно (offline), CVE — best-effort при
|
||||
доступности фида. Самоприменение CLAUDE.md §8 без человека.
|
||||
- Плата: ещё один «скрытый» под-гейт ребра (нет в `STAGE_TRANSITIONS`); внешние инструменты
|
||||
(gitleaks в образе, pip-audit в зависимостях); время скана на каждом прогоне (ограничено
|
||||
таймаутом); v1 — Python-only (SAST/мульти-стек — follow-up WI).
|
||||
- Сквозное изменение (новый QG + edge-под-гейт) → `arch:major-change`; прод-деплой ORCH-022 —
|
||||
строго через staging-гейт (8501), без рестарта прод-контейнера.
|
||||
|
||||
## Связи
|
||||
adr-0006 (merge-gate — паттерн edge-под-гейта/отката), adr-0008 (image-freshness —
|
||||
условность/never-raise/fail-closed), adr-0003 (условный гейт / `is_self_hosting_repo`),
|
||||
adr-0009 (анти-петля ложных FAIL, ORCH-061), ORCH-046 (дословный reason в `task_desc`),
|
||||
ORCH-9/15 (мульти-стек — будущая зависимость), ORCH-2 (worktree-изоляция).
|
||||
63
docs/architecture/adr/adr-0013-merge-verify-gate.md
Normal file
63
docs/architecture/adr/adr-0013-merge-verify-gate.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# adr-0013: Merge-в-main + пост-деплой верификация как условие `done` (фикс фантомного merge)
|
||||
|
||||
- **Статус:** accepted
|
||||
- **Дата:** 2026-06-08
|
||||
- **Задача:** ORCH-071 (CRITICAL bug)
|
||||
- **Детальный ADR:** `docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md`
|
||||
- **Постмортем:** `docs/history/LESSONS_2026-06-08_phantom-merge.md`
|
||||
|
||||
## Контекст
|
||||
Для self-hosting репо `orchestrator` стадия `deploy` идёт детерминированным путём
|
||||
(`_handle_self_deploy_phase_b → initiate_deploy → run_deploy_finalizer`), а LLM-агент
|
||||
`deployer` НЕ запускается. Фактический merge PR в `main` исторически делал **только**
|
||||
агент `deployer` → на self-hosting пути **нет шага merge-в-main вообще**. Detached
|
||||
host-деплой лишь retag'ает образ + рестартит 8500; `done` достигается по
|
||||
`deploy_status: SUCCESS` без верификации `main`. «Зелёный» деплой (образ из рабочей
|
||||
ветки) маскирует отсутствие merge → следующая задача срезает ветку от устаревшего `main`
|
||||
и теряет код предшественника. Накопительно потеряны ORCH-022/059/066/068. Вторичный
|
||||
фактор: Phase B рестартит прод → merge внутри живого процесса гонялся бы с рестартом
|
||||
(урок №3).
|
||||
|
||||
## Решение
|
||||
Детерминированный **merge-актор + пост-merge верификация** как **под-гейт ребра
|
||||
`deploy → done`**, врезанный в единственную функцию перехода `advance_stage` (симметрично
|
||||
edge-под-гейтам security/merge-gate/image-freshness). `STAGE_TRANSITIONS`,
|
||||
`check_deploy_status`/`_parse_deploy_status`, реестр `QG_CHECKS`, схема БД — **не меняются**.
|
||||
|
||||
- **Врезка `_handle_merge_verify` в `advance_stage`** (`current_stage=="deploy"` и
|
||||
`next_stage=="done"`, ПОСЛЕ зелёного `check_deploy_status`, ДО `update_task_stage`).
|
||||
Гейтит **ВСЕ** пути к `done` единообразно: `run_deploy_finalizer` (Phase C), reconciler
|
||||
F-1, job-reaper — все идут через `advance_stage`. Закрывает дыру: reconciler F-1 иначе
|
||||
протолкнул бы `done` в обход merge.
|
||||
- **Merge в Phase C (после рестарта), НЕ в Phase B.** Phase C finalizer —
|
||||
restart-surviving (reserved-job `deploy-finalizer`, claim воркером нового контейнера,
|
||||
re-drive reaper'ом). Merge физически строго ПОСЛЕ рестарта → рестарт его не убивает
|
||||
(G3 вторым вариантом — «шаг, переживающий рестарт»).
|
||||
- **Merge-актор `merge_gate.merge_pr`** — `pr_already_merged` (no-op повтор, ORCH-065) →
|
||||
иначе Gitea `POST /repos/{owner}/{repo}/pulls/{index}/merge`. Никогда push/force-push в
|
||||
`main`. never-raise.
|
||||
- **Верификатор `merge_gate.verify_merged_to_main`** — `PR.merged==true` ИЛИ
|
||||
`git merge-base --is-ancestor <validated_sha> origin/main`. never-raise → `False`
|
||||
(«не подтверждено»).
|
||||
- **Не подтверждено → alert «deploy succeeded but not merged» (Telegram+Plane) + HOLD**
|
||||
(`set_issue_blocked`, задача НЕ `done`, БЕЗ авто-отката на `development` — not-merged
|
||||
есть инфра-дефект, реакция ALERT-only как ORCH-021 self-hosting). Подтверждено →
|
||||
штатный `deploy → done` (терминал-sync / post-deploy monitor как сегодня) +
|
||||
`merged_to_main: true` во frontmatter `14-deploy-log.md` (наблюдаемость, `deploy_status:`
|
||||
нетронут).
|
||||
- **Идемпотентность (INV-5):** `pr_already_merged` перед merge; verify зелёный для
|
||||
уже-слитого PR; повтор без дубль-merge/ложного отката.
|
||||
- **Условность (как ORCH-35/43/58):** `merge_verify_enabled` (kill-switch, дефолт `true`) +
|
||||
`merge_verify_repos` (пусто → только self-hosting). Non-self репо — no-op, merge остаётся
|
||||
за агентом `deployer`.
|
||||
|
||||
## Инварианты
|
||||
never-raise на verify/merge (ошибка → alert, не падение конвейера); не рестартить/не ронять
|
||||
прод 8500; ручной approve прод-деплоя сохранён (`Confirm Deploy`, ORCH-059); только PR-merge
|
||||
API Gitea; restart-safe (sentinel + jobs, без миграции БД).
|
||||
|
||||
## Последствия
|
||||
Невозможно «`done` + прод задеплоен, а PR `open`». Минусы: при недоступной Gitea verify
|
||||
консервативно `False` → возможен ложный HOLD+alert (снимается повтором; fail-closed для
|
||||
`done` приоритетен); HOLD требует ручного вмешательства. Диагностика фантома — runbook
|
||||
`docs/operations/PHANTOM_MERGE_RUNBOOK.md` (G4).
|
||||
@@ -0,0 +1,77 @@
|
||||
# adr-0014: SHA-в-main — единственный критерий merge-verify + регресс-гард целостности `main`
|
||||
|
||||
- **Статус:** accepted
|
||||
- **Дата:** 2026-06-08
|
||||
- **Задача:** ORCH-073 (BUG CRITICAL — эрозия `main`)
|
||||
- **Amends:** [adr-0013](adr-0013-merge-verify-gate.md) (ORCH-071) — меняет КРИТЕРИЙ подтверждения merge.
|
||||
- **Детальный ADR:** `docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md`
|
||||
- **Постмортем:** `docs/history/LESSONS_2026-06-08_phantom-merge.md`
|
||||
|
||||
## Контекст
|
||||
|
||||
adr-0013 (ORCH-071) ввёл под-гейт merge-verify на ребре `deploy → done`, но допускал
|
||||
подтверждение merge по **ИЛИ-критерию**: `verify_merged_to_main` возвращал `True`, если
|
||||
`pr_already_merged(repo, branch)` **ЛИБО** SHA — предок `origin/main`. `pr_already_merged`
|
||||
засчитывал **любой** merged PR ветки, включая авто docs-PR (staging/deploy-логи). У одной
|
||||
feature-ветки в `main` сливались только docs-PR, а code-PR — нет → `pr_already_merged`=`True` →
|
||||
verify `CONFIRMED` → `done`, хотя кода в `main` не было. Накопительно потеряны ORCH-067 (ссылки
|
||||
`plane_issue_link`) и ORCH-069 (`qg0_title_max`). Вторичный усилитель — CHANGELOG-ребейзы,
|
||||
откатывающие ветку и тащащие устаревший код-сосед. Восстановление кода (G1) выполнено вручную
|
||||
restore-PR #76; этот ADR устраняет корень навсегда.
|
||||
|
||||
## Решение
|
||||
|
||||
1. **SHA-в-main — единственный критерий (FR-1).** `verify_merged_to_main(repo, branch, sha)`
|
||||
подтверждает merge **ТОЛЬКО** прямым фактом `git merge-base --is-ancestor <sha> origin/main`
|
||||
(после `git fetch origin main`). OR-ветка `pr_already_merged` **удалена** из верификатора.
|
||||
Пустой `sha` / любая git-ошибка → `False` (fail-closed: alert + HOLD). never-raise (INV-1).
|
||||
2. **`pr_already_merged` → idempotency-guard, различающий code-PR/docs-PR (FR-2).** Засчитывает
|
||||
merged PR только при `head.ref==<feature-branch>` И `base.ref=="main"` (явный фильтр в цикле,
|
||||
не ненадёжный query-параметр `head`). Используется лишь как защита `merge_pr` от второго merge,
|
||||
НЕ как подтверждение `done`.
|
||||
3. **`merge_pr` сливает именно code-ветку (FR-3).** Выбор открытого PR по `head.ref==branch` И
|
||||
`base.ref=="main"`; merge только Gitea `POST /pulls/{index}/merge`, никогда push/force-push в
|
||||
`main`. Источник истины «слилось» — FR-1.
|
||||
4. **Регресс-гард целостности `main` (FR-5).** Новая `merge_gate.check_main_regression`,
|
||||
вызываемая в `_handle_merge_verify` ПОСЛЕ подтверждённого SHA-в-main и ДО `done`: проверяет, что
|
||||
`origin/main` содержит **декларативный набор маркеров** ключевых функций ранее-merged задач
|
||||
(`git grep -c <marker> origin/main -- <path>` > 0). Маркер отсутствует → **alert «main
|
||||
regressed» + HOLD** (НЕ `done`, БЕЗ авто-отката на `development` — инфра-дефект, ALERT-only как
|
||||
ORCH-021/071). Набор — append-only константа `MAIN_REGRESSION_MARKERS` в `merge_gate.py`
|
||||
(расширяется каждой значимой задачей). **Fail-open** на git-ошибке самого грепа (регресс
|
||||
утверждается только при детерминированном `count==0`); первичный фейл-клозед — SHA-в-main.
|
||||
Kill-switch `regression_guard_enabled` (дефолт `true`); non-self → no-op.
|
||||
5. **`.gitattributes CHANGELOG.md merge=union` (FR-4).** В корне репо; авто-слияние правок
|
||||
`## [Unreleased]` без конфликта → `auto_rebase_onto_main` не откатывает ветку и не тащит
|
||||
устаревший код-сосед. `docs/**/*.md` под union **НЕ** ставится (union только для append-only;
|
||||
доки переписываются построчно).
|
||||
|
||||
## Инварианты
|
||||
|
||||
never-raise на verify/merge/регресс-гарде (ошибка → alert/HOLD, не падение); прод 8500 не
|
||||
рестартится/не падает в рамках merge; merge только Gitea PR-API без force-push в `main`; ручной
|
||||
`Confirm Deploy` (ORCH-059) сохранён; идемпотентность по «SHA-в-main», а не по «любому merged PR»;
|
||||
non-self репо (enduro) — merge/verify/регресс-гард без изменений. `STAGE_TRANSITIONS`, реестр
|
||||
`QG_CHECKS`, `check_deploy_status`, схема БД, внешние HTTP-эндпоинты — **без изменений**.
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- Сохранить PR-флаг как со-критерий verify (с фильтром head/base) — отклонено: PR можно слить и
|
||||
тут же откатить ребейзом-соседом; надёжен только факт «SHA в main».
|
||||
- `docs/**/*.md merge=union` — отклонено: тихая дубликация строк в переписываемых доках.
|
||||
- Регресс-гард с авто-откатом / хранением маркеров в БД/Plane — отклонено (Не-цель «не менять
|
||||
схему БД/Plane»; реакция ALERT-only).
|
||||
- Fail-closed на marker-grep — отклонено: ложный HOLD при git-сбое; marker-grep вторичен.
|
||||
|
||||
## Последствия
|
||||
|
||||
Невозможно «`done` + прод задеплоен, а code-PR не в `main`». Ложно-зелёный по docs-PR устранён в
|
||||
корне. CHANGELOG-конфликты больше не откатывают ветку. Регресс соседнего кода ловится отдельным
|
||||
гардом. Минус: при недоступной Gitea/git verify консервативно `False` → возможен ложный HOLD+alert
|
||||
(снимается повтором; fail-closed для `done` приоритетен). Набор маркеров требует дисциплины —
|
||||
значимая задача дописывает свой маркер.
|
||||
|
||||
## Связи
|
||||
|
||||
- Amends adr-0013 (ORCH-071), наследует adr-0006 (merge-gate), adr-0011 (job-reaper/lease).
|
||||
- Детально: `docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md`.
|
||||
@@ -0,0 +1,47 @@
|
||||
# adr-0015: Зависимости задач + сериализация merge внутри репо
|
||||
|
||||
**Статус:** accepted · **Дата:** 2026-06-08 · **Источник:** ORCH-026
|
||||
**Связи:** дополняет adr-0006 (merge-gate), adr-0011 (merge-lease + reclaim), adr-0013/0014
|
||||
(merge-verify, SHA-in-main), adr-0002 (очередь). Детально —
|
||||
`docs/work-items/ORCH-026/06-adr/ADR-001-merge-serialization-and-task-deps.md`.
|
||||
|
||||
## Контекст
|
||||
|
||||
Эрозия `main` 08.06 родилась из некоординированного параллелизма задач одного репо (ветки от
|
||||
устаревшего `main`, фантом-merge затирает соседа). adr-0014 закрыл последствия; ORCH-026 — корень
|
||||
на уровне планировщика. Плюс исходный скоуп ORCH-026: декларативные зависимости задач (B ждёт A).
|
||||
|
||||
## Решение
|
||||
|
||||
**Уровень A — сериализация merge/деплоя (per-repo).** Окно сериализации уже обеспечивается
|
||||
merge-lease (adr-0011): захват в `check_branch_mergeable`, удержание до release (PR-merged webhook /
|
||||
`deploy→done`=SHA-in-main для self / откат / проактивный reclaim). Это и есть окно
|
||||
«merge → main-updated» — **механизм не переписывается**. Добавляется единственное новое поведение:
|
||||
**безусловный proactive pre-merge rebase** (флаг `premerge_rebase_always`, дефолт `True`, скоуп
|
||||
`merge_gate_repos`): под лизом всегда вызывается `auto_rebase_onto_main` (no-op + «Everything
|
||||
up-to-date» на актуальной ветке → CI не триггерится; реальный догон на отстающей). Инвариант:
|
||||
никаких push в `main`, force только `--force-with-lease` на ветку.
|
||||
|
||||
**Уровень B — декларативные зависимости.** Аддитивная таблица `job_deps(task_id,
|
||||
depends_on_task_id)` — **источник истины планировщика** (offline-устойчивость: сетевой Plane в
|
||||
горячем claim встанет очередью всех проектов). Источник декларации настраивается
|
||||
`task_deps_source = db|plane|hybrid` (дефолт `db`); планировщик всегда читает БД-кэш. Гейт —
|
||||
условие `NOT EXISTS` в `claim_next_job` (задача не выбирается, пока есть незавершённая зависимость;
|
||||
слот `max_concurrency` не занимается). Циклы — DFS-детектор (`src/task_deps.py`) + `set_issue_blocked`
|
||||
+ alert. Видимость — строка «⏳ ждёт ORCH-NNN» в Telegram-карточке (Plane Blocked — на дедлоке).
|
||||
Зависимости — только intra-repo (v1).
|
||||
|
||||
## Альтернативы
|
||||
|
||||
Отдельный merge-lock/merge-queue (дублирует adr-0011); расширение release-точек лиза (не нужно —
|
||||
окно уже корректно); Plane как источник истины планировщика (self-hosting risk); гейт зависимостей
|
||||
в воркере с claim+requeue (churn vs. чистый `NOT EXISTS`); поле в `tasks` вместо таблицы (M:N хуже).
|
||||
|
||||
## Последствия
|
||||
|
||||
Минимально-инвазивно: `STAGE_TRANSITIONS`/`QG_CHECKS` не тронуты (паттерн врезки), переиспользует
|
||||
merge-gate/merge-lease целиком. Обе фичи инертны без данных → нулевая регрессия для enduro-trails.
|
||||
restart-safe, never-raise, kill-switch на каждую (`premerge_rebase_always`, `task_deps_enabled`).
|
||||
Миграция — только аддитивная (`CREATE TABLE/INDEX IF NOT EXISTS`). Ограничение: B v1 — intra-repo.
|
||||
Self-hosting safety: изменения идут через `deploy-staging` → `Confirm Deploy`, без внеочередного
|
||||
рестарта прода.
|
||||
@@ -0,0 +1,52 @@
|
||||
# ADR-0016: ensure_open_pr — гарантированный код-PR перед merge-verify (ORCH-082)
|
||||
|
||||
## Статус
|
||||
Accepted — амендмент к [adr-0013](adr-0013-merge-verify-gate.md) и
|
||||
[adr-0014](adr-0014-merge-verify-sha-source-of-truth.md). Детально:
|
||||
`docs/work-items/ORCH-082/06-adr/ADR-001-ensure-open-pr-before-merge-verify.md`.
|
||||
|
||||
## Контекст
|
||||
Merge-verify (ORCH-071/073) — под-гейт ребра `deploy → done`: детерминированно мержит код-PR в
|
||||
`main` (`merge_pr`) и подтверждает merge **только** по «SHA-в-main» (`verify_merged_to_main`,
|
||||
ORCH-073). На деплое ORCH-074 (08.06) `merge_pr` вернул `("False", "no open PR")`: у ветки **не
|
||||
было** открытого PR с `head==branch` И `base=="main"`. Защита ORCH-073 верно удержала задачу
|
||||
(HOLD, не ложный `done`), но это лечило **следствие**.
|
||||
|
||||
Первопричина (код-аудит): PR создаётся в конвейере **единственной** функцией
|
||||
`launcher._ensure_pr`, вызываемой **только** на developer-пути и **только** при свежем
|
||||
worktree-коммите. Любой сценарий без свежего developer-коммита (бойнс без правок, повторный
|
||||
прогон, **ручное восстановление ветки/`main`** — случай ORCH-074) оставляет ветку без код-PR.
|
||||
Инвариант «к merge-verify у ветки есть открытый код-PR» в конвейере **отсутствовал** → блокер
|
||||
автономного деплоя (ORCH-54).
|
||||
|
||||
## Решение
|
||||
Аддитивно обеспечить инвариант **внутри того же под-гейта**, ПЕРЕД `merge_pr`, не трогая машину
|
||||
стадий:
|
||||
|
||||
1. **Новый leaf-актор `merge_gate.ensure_open_pr(repo, branch) -> (status, detail)`** (never-raise):
|
||||
`GET …/pulls?state=open` с фильтром **`head.ref==branch` И `base.ref=="main"`** (идентичен
|
||||
`merge_pr`/ORCH-073 FR-3 — авто-docs-PR не считается код-PR) → `("existed", N)`; иначе
|
||||
`POST …/pulls` → `("created", N)`; гонка «PR exists» → повторный GET → `existed` (без дублей);
|
||||
любая ошибка → `("failed", reason)`.
|
||||
2. **Врезка в `_handle_merge_verify`** ПОСЛЕ резолва `validated_revision` и ПЕРЕД `merge_pr`:
|
||||
`created|existed` → штатно к `merge_pr`; `failed` → честный HOLD+alert через новый helper
|
||||
`_hold_pr_create_failed` (текст «PR создать не удалось» — отличим от not-merged HOLD), задача
|
||||
остаётся на `deploy`, БЕЗ отката на development.
|
||||
3. **Kill-switch `merge_verify_autocreate_pr_enabled`** (дефолт `True`); область —
|
||||
`merge_verify_applies` (self-hosting / `merge_verify_repos`). `False` → поведение ORCH-074 1:1.
|
||||
4. **`launcher._ensure_pr`** рекомендуется делегировать в `ensure_open_pr` (единый код создания
|
||||
PR), сохранив прежний триггер «только developer-путь».
|
||||
|
||||
## Последствия
|
||||
- **Защита ORCH-073 неприкосновенна и приоритетна:** подтверждение merge остаётся ТОЛЬКО
|
||||
`verify_merged_to_main` (SHA-в-main) + `check_main_regression`. Создание PR устраняет лишь
|
||||
**ложный** HOLD «no open PR», но не маскирует реально невлитый код (тот → HOLD как прежде).
|
||||
- **Без миграций:** идемпотентность выводится из Gitea (наличие открытого PR), схема БД не меняется
|
||||
— restart-safe; повторный заход (reaper/reconciler/re-approve) → `existed`, дублей нет.
|
||||
- **Инварианты целы:** `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, `check_deploy_status`,
|
||||
exit-коды хука, merge-gate (ORCH-043), image-freshness (ORCH-058) — без изменений; `main` не
|
||||
push/force-push; never-raise на всём пути.
|
||||
- **Наблюдаемость:** один однозначный исход в логах на проход — created / existed / failed; HOLD по
|
||||
failed текстуально отличим от HOLD not-merged.
|
||||
- **Минус:** код-PR может создаваться после прохождения гейтов — безопасно, т.к. гейты валидируют
|
||||
код ветки, а merge-verify идёт ПОСЛЕ всех гейтов; PR — лишь механизм слияния, ревью не обходится.
|
||||
59
docs/architecture/adr/adr-0017-serial-gate.md
Normal file
59
docs/architecture/adr/adr-0017-serial-gate.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# adr-0017: Per-repo serial gate (пакетный автономный режим, serial e2e)
|
||||
|
||||
Статус: **proposed** · Дата: 2026-06-09 · Источник: **ORCH-088** (Этап 1)
|
||||
Детально: `docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`.
|
||||
|
||||
## Контекст
|
||||
Цель эпика ORCH-088 — масштаб автономности: накидать вечером 10–20 задач и получить к утру пакет,
|
||||
последовательно проведённый через весь конвейер (analysis → … → deploy → done). Корневая проблема —
|
||||
**stale-анализ**: ветка задачи N+1 срезается на входе в анализ (`start_pipeline._create_gitea_branch`)
|
||||
от `main`, ещё не содержащего код предшественника N. Физическое код-затирание уже закрыто (ORCH-026
|
||||
auto_rebase + merge-lease); остаётся **логический** разрыв. Plane API v1 не имеет bulk/relations ⇒
|
||||
очередь/зависимости хранятся у оркестратора (gate по локальной БД).
|
||||
|
||||
## Решение
|
||||
**Per-repo serial gate** — новая задача репо не входит в `analysis` (не режет ветку, не запускает
|
||||
analyst), пока в том же репо есть незавершённая задача (`stage != 'done'`) или репо заморожен.
|
||||
Три механизма, аддитивно, под kill-switch, с областью репо, never-raise, restart-safe:
|
||||
|
||||
1. **Gate-в-claim** (`db.claim_next_job`) — analyst-job (`jobs.agent='analyst'`) применимого репо не
|
||||
выбирается, если `EXISTS` другая незавершённая задача репо ИЛИ активна строка `repo_freeze`. По
|
||||
образцу `task_deps` `NOT EXISTS` (ORCH-026); только локальная БД (offline hot-path). Job'ы уже
|
||||
активной задачи проходят свободно; rework-analyst не блокирует себя (`t2.id != jobs.task_id`).
|
||||
2. **Отложенный срез ветки** — для применимого репо `start_pipeline` создаёт task-row + enqueue
|
||||
analyst, но **не** создаёт Gitea-ветку/docs; срез релоцируется на момент claim analyst-job
|
||||
(launcher), когда `origin/main` уже содержит предшественника (`done` ⇔ SHA-в-main, ORCH-071/073).
|
||||
`ensure_worktree` режет от свежего `origin/main` ⇒ AC-6 структурно. Идемпотентно (409 = no-op).
|
||||
3. **Durable per-repo freeze** (`repo_freeze`) — post-deploy `DEGRADED`/rollback (ORCH-021) →
|
||||
`set_repo_freeze` + Telegram-алерт; gate закрыт безусловно до **ручного** снятия
|
||||
(`POST /serial-gate/unfreeze`). Деградировавшая задача уже `done` (BR-7) ⇒ нужен отдельный сигнал.
|
||||
|
||||
Чистая логика — leaf `src/serial_gate.py` (never-raise). Флаги `serial_gate_enabled` (kill-switch),
|
||||
`serial_gate_repos` (CSV; **пусто ⇒ все репо**, в отличие от self-hosting-only ORCH-35/43/58),
|
||||
`serial_gate_freeze_enabled`. Наблюдаемость — блок `serial_gate` в `GET /queue`.
|
||||
|
||||
## Альтернативы
|
||||
- **Гейт в `start_pipeline` + re-trigger при `done`** — больше состояния/путей, риск зависших задач;
|
||||
relocation на claim переиспользует restart-safe `jobs`-очередь.
|
||||
- **Freeze как колонка `tasks`** — неверная семантика (freeze per-repo, задача уже `done`).
|
||||
- **Self-hosting-only область** — лишает enduro анти-stale-base (FR-3).
|
||||
- **Отдельная таблица очереди ожидания** — избыточно; `jobs(queued)`+gate достаточно.
|
||||
- **Снятие freeze Plane-жестом** — перегрузка статусов (анти-паттерн ORCH-059).
|
||||
|
||||
## Последствия
|
||||
- **+** AC-6 закрыт структурно; AC-2/AC-3 «бесплатны» (ожидание = `queued` job без ветки);
|
||||
переиспользование проверенных паттернов; cross-repo параллелизм сохранён; `STAGE_TRANSITIONS` /
|
||||
`QG_CHECKS` / `check_*` / merge-gate / merge-verify / image-freshness / post-deploy / deploy-хук /
|
||||
`max_concurrency` — **без изменений**.
|
||||
- **NFR-1:** hot-claim тотальный сбой → **fail-open** (не заклинить очередь всех проектов); freeze в
|
||||
Python-слое → **fail-closed** (безопасность прода).
|
||||
- **−** Срез ветки/docs мигрируют из async в sync-путь launcher (обёртка); Blocked-задача держит пакет
|
||||
(Этап 1, осознанно); freeze снимается только вручную.
|
||||
- Откат: `serial_gate_enabled=False` ⇒ claim/старт 1:1 как до ORCH-088; таблица `repo_freeze` инертна.
|
||||
- **Вне скопа** (Этап 1): merge-очередь FIFO, pre-merge rebase как отдельная фича, фазы A/B/C,
|
||||
любой параллелизм задач внутри одного репо, зависимость от ORCH-83.
|
||||
|
||||
## Связи
|
||||
- Переиспользует: adr-0002 (очередь ORCH-1), adr-0015 (claim-gate/auto_rebase/merge-lease ORCH-026),
|
||||
adr-0010 (post-deploy monitor — источник DEGRADED), adr-0013/0014 (merge-verify ⇒ `done`⇔SHA-в-main).
|
||||
- Новая аддитивная таблица `repo_freeze` (`docs/work-items/ORCH-088/08-data-requirements.md`).
|
||||
@@ -107,6 +107,33 @@ claude.exe --print --system-prompt --allowedTools Read,Write,Edit,Bash
|
||||
2. Если < MAX_DEV_RETRIES (3) — откатывает в development, перезапускает developer
|
||||
3. Если >= MAX_DEV_RETRIES — эскалация (логирование + уведомление)
|
||||
|
||||
### 7. Live Telegram tracker (`src/notifications.py`)
|
||||
|
||||
Вместо ~15 отдельных сообщений на задачу оркестратор держит **ОДНУ** live-карточку на задачу (`update_task_tracker`), которая обновляется на каждом переходе стадии. Текст рендерится статически из БД (`render_task_tracker`: стадии, токены, стоимость, BRD-подтверждение, итоги). Карточка всегда тихая (`disable_notification=True`); отдельные пинги шлют только `notify_approve_requested` / `notify_error`. `message_id` хранится в `tasks.tracker_message_id`; helpers `get_tracker_message_id` / `set_tracker_message_id`. Контракт всего компонента — **never raises**.
|
||||
|
||||
**Режимы (ORCH-042, `ORCH_TRACKER_MODE` → `Settings.tracker_mode`; дефолт переключён `edit → bump` в ORCH-067).** Резолвится в `update_task_tracker` (case-insensitive, trim); всё, что ≠ `"bump"` (включая пустое/мусор/None), трактуется как `edit` → безопасный фолбэк. Инвариант «одна карточка на задачу» сохраняется в обоих режимах.
|
||||
|
||||
| Режим | Поведение при обновлении |
|
||||
|-------|--------------------------|
|
||||
| `bump` (дефолт, ORCH-067) | карточка пересоздаётся внизу чата: best-effort `delete_telegram(старый_id)` → `send_telegram(text, disable_notification=True)` → `set_tracker_message_id(new_id)` **только** при успешном send (`new_mid is not None`). За один вызов — не более одного нового сообщения. Живая карточка всегда «догоняет» переписку. |
|
||||
| `edit` | первый вызов → `send_telegram` (тихо) + сохранение `message_id`; далее → `edit_telegram` на сохранённый id. Новое сообщение шлётся ТОЛЬКО при `EDIT_GONE` (удалено/старше 48ч/невалидный id). `EDIT_NOT_MODIFIED` / `EDIT_FAILED` → нового сообщения нет (анти-дубль). |
|
||||
|
||||
**`delete_telegram(message_id) -> bool`** (low-level, never raises). Семантика возврата — «исчезло ли старое сообщение»:
|
||||
- `ok:true` → `True`;
|
||||
- `ok:false` с маркерами `_DELETE_GONE_MARKERS` (`message to delete not found`, `message can't be deleted`, `message_id_invalid`) → `True` (старше 48ч / уже удалено — не транзиент);
|
||||
- прочий `ok:false` / 5xx / исключение (сеть/таймаут) → `False` + `logger.warning`;
|
||||
- нет токена/chat_id → `False`, HTTP не выполняется.
|
||||
|
||||
Результат `delete_telegram` **не** блокирует отправку новой карточки (BR-6: delete-fail у сообщения >48ч → всё равно шлём новое); `False` означает лишь «старое, возможно, ещё живо» — будет вычищено повторной попыткой на следующем переходе. При транзиентном сбое send (`None`) указатель `tracker_message_id` **не** затирается (анти-затирание, симметрично edit-fallback).
|
||||
|
||||
**Текст карточки (оба режима, ORCH-042):** метка `Подтверждение BRD` (была «Ревью БРД»); после прохождения approve-gate строка BRD начинается с ✅ (ветка ожидания сохраняет ⏸️/⏳); русские display-labels стадий (`Анализ / Архитектура / Разработка / Код ревью / Тестирование / Внедрение`); финальная строка `📦 Внедрено` (было `deployed`). Меняются только отображаемые строки — ключи стадий и имена агентов (завязаны на `_STAGE_ACTIVE_AGENT`, `last_done`, БД) не трогаются.
|
||||
|
||||
**Строка Plane-статуса и кликабельный номер (ORCH-067, слой B — индикация).** Под заголовком карточка несёт строку `📍 <Plane-статус>` по модели ORCH-066. Источник — двухслойный, контракт **never raises**:
|
||||
- **Оффлайн-ядро** `plane_status_label(task_row)` — чистая функция БЕЗ сети: `stage → статус` (`created→To Analyse`, `analysis→Analysis`, `architecture→Architecture`, `development→Development`, `review→Code-Review`, `testing→Testing`, `deploy→⏸️ Awaiting Deploy`, `done→Done`) + `⏸️ In Review` из brd-часов (`brd_review_started_at` задан, `…_ended_at` пуст). Неизвестная/битая стадия → безопасный дефолт `To Analyse`.
|
||||
- **Live-overlay** `_live_plane_branch_override` — best-effort: дорисовывает ветви-статусы, неразличимые оффлайн (Needs Input / Blocked / Rejected / Cancelled / Deploying / Monitoring after Deploy), чтением живого Plane-статуса (`fetch_issue_state` с коротким `tracker_live_status_timeout_s`, TTL-кэш `tracker_live_status_ttl_s`, kill-switch `tracker_live_status`). Любой сбой / выключенный флаг / нехватка данных → оффлайн-метка; `⏸️ In Review` (авторитет brd-часов) overlay не консультирует. Анти-false-positive: `deploying/monitoring`, алиасящие базовый UUID на проекте без выделенного статуса (enduro), не вызывают override.
|
||||
|
||||
**Кликабельный номер задачи (ORCH-067).** Номер в заголовке карточки И во всех уведомлениях орка, где упоминается `work_item_id`, — HTML-ссылка на issue в Plane через общий `plane_issue_link` / `link_for` (URL строит `_plane_issue_url` с loopback/workspace/project-гардами, переиспользуя резолв ORCH-017). Fail-safe: при нехватке любого из (web-base/не-loopback, workspace, project_id, plane_issue_id) → `html.escape(work_item_id)` без `<a>`; динамические части экранируются, `<a>`-разметка валидна под `parse_mode=HTML`. Алерты `stage_engine`/`launcher`/`security_gate`/`reconciler` переведены на `link_for` (резолвит `repo`+`plane_issue_id` из БД по `task_id` или `work_item_id`).
|
||||
|
||||
## Database Schema
|
||||
|
||||
```sql
|
||||
@@ -305,6 +332,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` | последняя ошибка |
|
||||
|
||||
@@ -322,6 +350,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.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Lessons Learned — 2026-06-08: статус `Confirm Deploy` не триггерит Phase B (мёртвый триггер)
|
||||
|
||||
## Контекст
|
||||
ORCH-066 ввела новую статусную модель Plane, включая человекочитаемый статус **`Confirm Deploy`** для прод-деплойного approve-gate (self-deploy Phase B). Орк сам выставляет задачу в `Awaiting Deploy` / `Confirm Deploy` через `set_issue_awaiting_deploy()` и т.п.
|
||||
|
||||
## Инцидент (2026-06-08, первый реальный прод-self-deploy — ORCH-068)
|
||||
Слава нажал статус **`Confirm Deploy`** в Plane, ожидая запуск прод-деплоя. Орк ответил `no pipeline action` и НИЧЕГО не запустил. Прод-деплой стартовал только после ручного перевода в **`Approved`**.
|
||||
|
||||
## Root cause
|
||||
Диспетчер статусов `handle_issue_status` (`src/webhooks/plane.py` ~158-166) слушает РОВНО три состояния:
|
||||
```python
|
||||
if new_state == proj_states["to_analyse"]: await handle_status_start(...)
|
||||
elif new_state == proj_states["approved"]: await handle_verdict(..., approved=True)
|
||||
elif new_state == proj_states["rejected"]: await handle_verdict(..., approved=False)
|
||||
else: logger.info("... no pipeline action")
|
||||
```
|
||||
Phase B (прод-деплой) триггерится в `_try_advance_stage` (`src/stage_engine.py` ~215-224) при `current_stage == "deploy" and finished_agent is None` — то есть ТОЛЬКО когда пришёл вебхук `Approved`. Статус `Confirm Deploy` в эту тройку НЕ входит → ветка `else` → no-op.
|
||||
|
||||
**ORCH-066 добавила статус как МЕТКУ (запись), но не подключила обратный путь (чтение/триггер).** Классическая дыра: протестировали, что орк правильно СТАВИТ статус, но не протестировали, что нажатие этого статуса человеком РЕАЛЬНО запускает действие.
|
||||
|
||||
## Почему не поймали тестирование/ревью
|
||||
1. **Не в scope ORCH-068.** ORCH-068 чинит reconciler (BRD §6 N1-N3 явно: не трогать диспетчер статусов / Phase B). Тестер прогнал TC-01..13 — все про reconciler/terminal-статусы. Ревьюер смотрел diff reconciler.py/plane_sync.py. Корректно — это дефект ORCH-066, не 068.
|
||||
2. **Дыра ORCH-066.** Её тесты, видимо, проверяли запись статусов, а не обратный триггер.
|
||||
3. **Staging не покрывает прод-путь.** Phase A (staging-деплой) автоматический, ручной `Confirm Deploy` живёт ТОЛЬКО на прод-пути, который на staging не гоняется. Поэтому всплыло лишь на первом реальном прод-деплое.
|
||||
|
||||
## Уроки
|
||||
1. **Тестировать обратный путь статусов, не только запись.** Для каждого статуса, который человек может нажать, нужен тест «нажатие → ожидаемое pipeline-действие». Запись (орк ставит статус) и чтение (орк реагирует на статус) — два разных контракта.
|
||||
2. **Прод-only пути (ручной Confirm Deploy) нуждаются в явном тесте/чеклисте.** Staging их не ловит by design. Любой approve-gate, доступный человеку, обязан иметь регресс-тест на триггер.
|
||||
3. **Новый статус = подключить В ОБЕ стороны.** При добавлении статуса в модель — сразу проверить, что диспетчер `handle_issue_status` его слушает (если он actionable), а не только что орк его выставляет.
|
||||
4. **UX-консистентность:** статус, названный действием («Confirm Deploy»), обязан выполнять это действие. Иначе оператор жмёт интуитивную кнопку, а система молчит → потеря доверия к автономности.
|
||||
|
||||
## Фикс
|
||||
Заведена ORCH-070: подключить `Confirm Deploy` (или его actionable-эквивалент) к триггеру Phase B в `handle_issue_status`, + регресс-тест на обратный путь статусов прод-деплоя. Source-of-truth и существующий `Approved`-путь не ломать (обратная совместимость).
|
||||
47
docs/history/LESSONS_2026-06-08_phantom-merge.md
Normal file
47
docs/history/LESSONS_2026-06-08_phantom-merge.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Lessons Learned — 2026-06-08: «Фантомный merge» — прод деплоится, но код не сливается в main
|
||||
|
||||
## Severity: CRITICAL (потеря целостности main, накопительная потеря кода между задачами)
|
||||
|
||||
## Резюме
|
||||
Self-deploy (Phase B) собирал прод-образ из ВЕТКИ задачи и рапортовал `finalize SUCCESS` + `post-deploy HEALTHY`, но git-merge ветки в `main` НЕ происходил. PR оставался `open`. Следующая задача срезала свою ветку от устаревшего main → теряла код незалитых предшественников. Накопительно потеряны в main: **ORCH-022, ORCH-059, ORCH-066, ORCH-068** (PR#67/68/69/70 — все open, merged=False). Последний реально слитый — ORCH-065 (PR#66).
|
||||
|
||||
## Как обнаружено
|
||||
Симптом: ORCH-067 переведён в `To Analyse`, но конвейер не стартовал (`no pipeline action`). Причина — прод слушал старый триггер `in_progress`, а не `to_analyse` (ORCH-066). При разборе выяснилось: код ORCH-066 не в проде, хотя он «деплоился».
|
||||
|
||||
Решающее наблюдение оператора (Слава): «спам ET-002 начался СРАЗУ после деплоя 66 → значит код деплоился». Это вскрыло механизм: код 66 БЫЛ в проде 22:17–05:32, потом стёрт деплоем 068 (срезан от старого main без 66).
|
||||
|
||||
## Доказательная база (как подтверждали — воспроизводимый метод)
|
||||
1. **PR-статус (Gitea API):** PR#67(022)/68(059)/69(066)/70(068) = open, merged=False. PR#66(065) = merged=True (последний честный).
|
||||
2. **md5-сверка файлов прод vs origin/main vs ветка:**
|
||||
- `src/reconciler.py`, `src/plane_sync.py`: prod md5 == ветка ORCH-068 != main → прод = снимок ветки 068, НЕ main.
|
||||
- `src/webhooks/plane.py`: prod == main == ветка-068 (ветка 068 этот файл не трогала → видна старая база без to_analyse).
|
||||
3. **git merge-base:** ветка ORCH-068 срезана от `bb03350` (ORCH-065), не от кода 066. История ветки-068 по 066 содержит только `docs staging`, кода (`to_analyse`) нет.
|
||||
4. **Таймлайн логов:** деплой 22:17 (ветка-066, сломанный reconciler) → спам ET-002 начался; деплой 05:32 (ветка-068, база 065 без 66) → спам прекратился (0 после 05:33). Подтверждает: прод-образ = снимок ВЕТКИ, меняется при каждом деплое, теряет незалитое.
|
||||
|
||||
## Root cause (гипотеза → нужен код-аудит self_deploy/merge_gate)
|
||||
Self-deploy Phase B инициирует прод-деплой из worktree ветки (BUILD-ONCE из validated commit). Шаг git-merge ветки в main:
|
||||
- ЛИБО не вызывается на self-hosting пути (Phase B уходит в detached host-процесс, finalizer пишет SUCCESS-маркеры, но merge отдельно и молча скипается/падает),
|
||||
- ЛИБО регресс фикса ORCH-065 (idempotent merge / merge-lease reclaim): guard `pr_already_merged` или lease-reclaim ошибочно считает PR уже слитым / не докатывает merge после рестарта контейнера (а Phase B ИМЕННО рестартит контейнер → процесс, державший merge-lease, умирает до завершения merge).
|
||||
Симптоматически ORCH-065 был последним успешным merge — деградация началась СРАЗУ после него или из-за взаимодействия его механики с self-deploy-рестартом.
|
||||
|
||||
## Почему конвейер не заметил
|
||||
- `finalize SUCCESS` и `post-deploy HEALTHY` маркеры пишутся НЕЗАВИСИМО от факта merge. Пайплайн считает задачу done по этим маркерам, git-состояние main не верифицируется.
|
||||
- Прод здоров (образ из ветки рабочий) → health-check зелёный → нет сигнала о проблеме.
|
||||
- Дыра видна только при сравнении main с прод ИЛИ когда следующая задача теряет код предыдущей (что и случилось с 67).
|
||||
|
||||
## Уроки
|
||||
1. **Деплой ОБЯЗАН верифицировать, что код реально в main ПОСЛЕ деплоя.** finalize SUCCESS без проверки `git merge-base origin/main == deployed_commit` (или PR.merged==true) — фальшивый зелёный. Добавить post-merge верификацию: deployed SHA должен быть предком origin/main.
|
||||
2. **Маркер «deployed» != «merged».** Нельзя считать задачу завершённой по staging/post-deploy-маркерам, если PR не закрыт merge. Гейт: задача → done ТОЛЬКО при PR.merged==true.
|
||||
3. **Self-deploy рестартит контейнер → любой держатель merge-lease/незавершённый git-шаг умирает.** Merge ДОЛЖЕН завершиться и быть подтверждён ДО рестарта прод-контейнера, либо merge выносится в шаг, переживающий рестарт (как requeue_running_jobs, но для merge-в-main).
|
||||
4. **Срез ветки от main делает целостность main критичной.** Если main отстаёт — каждая новая задача наследует дыру. main = единственный источник для новых веток, его рассинхрон с прод = накопительная потеря.
|
||||
5. **Метод диагностики (сохранить как runbook):** при подозрении на рассинхрон — (a) Gitea API PR list merged-флаги, (b) md5 prod-файлов vs `git show origin/main:<file>`, (c) merge-base ветки vs main, (d) таймлайн деплой-логов. Эти 4 проверки однозначно локализуют фантом.
|
||||
|
||||
## Действия
|
||||
- Восстановление main: интеграционная ветка `integ/restore-main-2026-06-08` — последовательный merge 022→059→066→068 (docs union-resolved, reconciler-конфликт 066⊕068 разрешён: каркас 068 livelock-fix + триггер to_analyse 066), полный pytest, затем merge в main + передеплой.
|
||||
- Заведён критбаг ORCH-071: «фантомный merge — self-deploy без верификации merge в main» (root-fix: post-deploy verify + done-гейт по PR.merged + merge до рестарта).
|
||||
- ORCH-070 (Confirm Deploy trigger) частично ДУБЛИРУЕТ ORCH-059 (handle_confirm_deploy уже написан в 059) — после долива 059 пересмотреть scope 070 (остаётся только display-слой статусов Monitoring after Deploy).
|
||||
|
||||
## Связанные
|
||||
- ORCH-065 (последний честный merge; подозрение на регресс его merge-механики)
|
||||
- ORCH-066/068 (потерянный код), ORCH-059 (Confirm Deploy trigger, тоже потерян)
|
||||
- Урок 2026-06-08 confirm-deploy-deadtrigger (симптом того же корня)
|
||||
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
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
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".
|
||||
@@ -16,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`.
|
||||
@@ -29,6 +42,13 @@
|
||||
| `TARGET_IMAGE` | `orchestrator-orchestrator-staging` | Имя образа для retag при rollback |
|
||||
| `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.
|
||||
@@ -55,6 +75,20 @@ PREV_IMAGE_FILE=/home/slin/repos/orchestrator/.deploy-prev-image-prod \
|
||||
bash scripts/orchestrator-deploy-hook.sh --deploy
|
||||
```
|
||||
|
||||
### Прод build-once (ORCH-036) — ретег staging-образа, без rebuild
|
||||
|
||||
Так прод-деплой запускается **автоматически** исполняемым самодеплоем (Фаза B: `ssh + setsid`, см. `INFRA.md`). Ключевое отличие — `SOURCE_IMAGE` указывает на провалидированный staging-образ, который ретегается на прод-тег:
|
||||
|
||||
```bash
|
||||
SOURCE_IMAGE=orchestrator-orchestrator-staging \
|
||||
TARGET_SERVICE=orchestrator \
|
||||
TARGET_PORT=8500 \
|
||||
TARGET_IMAGE=orchestrator-orchestrator \
|
||||
COMPOSE_PROFILE="" \
|
||||
PREV_IMAGE_FILE=/home/slin/repos/orchestrator/.deploy-prev-image-prod \
|
||||
bash scripts/orchestrator-deploy-hook.sh --deploy
|
||||
```
|
||||
|
||||
### Ручной rollback staging
|
||||
|
||||
```bash
|
||||
|
||||
@@ -30,12 +30,33 @@
|
||||
|
||||
Оба: `network_mode: host`, `init: true` (tini как PID 1 — reaping зомби, B-2), `restart: unless-stopped`.
|
||||
|
||||
### Рантайм-uid (ORCH-040)
|
||||
Оба сервиса бегут под `user: "1000:1000"` (slin), **не** root. Артефакты конвейера
|
||||
(git worktree `/repos/_wt/...`, коммиты в `docs/work-items/...`) создаются как
|
||||
`slin:slin`, поэтому `git pull` / `git reset` на хосте под slin работают без ручного
|
||||
`chown`. Доступ к docker.sock сохранён через `group_add: ["999"]` (gid docker, **не**
|
||||
через root — НЕ удалять). При переносе на другой хост uid пересматривается. См.
|
||||
ADR `docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md` и глобальный
|
||||
`docs/architecture/adr/adr-0005-container-runs-as-host-uid.md`.
|
||||
|
||||
**Host-prerequisites (обязательная процедура Owner, в git не коммитятся):**
|
||||
- **P-1 (блокер):** uid 1000 читает claude creds — `chown -R 1000:1000 /home/slin/.claude`;
|
||||
проверка `sudo -u '#1000' test -r /home/slin/.claude/.credentials.json`. Без этого
|
||||
preflight (ORCH-044) заворачивает весь конвейер.
|
||||
- **P-2:** ssh-ключи в `/home/slin/.orchestrator-ssh` читаемы uid 1000 (маунт ведёт в `/home/slin/.ssh`).
|
||||
- **P-3:** `id slin` → `1000:1000`; `/repos`, `/app/data` уже `1000:1000`.
|
||||
- **P-4:** прод-рестарт self — только в окно тишины (`GET /status` без активных задач):
|
||||
общий инстанс с enduro-trails.
|
||||
- Разовый разгребающий `chown -R 1000:1000 /home/slin/repos/orchestrator` для старых
|
||||
`root:root` файлов из истории (вне объёма кода).
|
||||
|
||||
### Тома (volumes)
|
||||
- `./data` → `/app/data` (БД; у staging — `./data/staging`)
|
||||
- `/home/slin/repos` → `/repos` (рабочие репозитории проектов)
|
||||
- `/var/run/docker.sock` (для docker-операций деплоя)
|
||||
- claude-code, node, `~/.claude*` (CLI агентов, ro)
|
||||
- `~/.orchestrator-ssh` → `/root/.ssh` (ro, деплой по ssh)
|
||||
- `~/.orchestrator-ssh` → `/home/slin/.ssh` (ro, деплой по ssh; target в HOME агента,
|
||||
согласован с `HOME=/home/slin` из launcher — ORCH-040, ранее `/root/.ssh`)
|
||||
|
||||
## Переменные окружения (карта; значения — в `.env`)
|
||||
|
||||
@@ -54,6 +75,22 @@
|
||||
| `ORCH_AGENT_EFFORT_DEFAULT` | режим работы `--effort` по умолчанию (ORCH-41): low\|medium\|high\|xhigh\|max; дефолт `high` |
|
||||
| `ORCH_AGENT_EFFORT_<AGENT>` | per-agent effort; дефолт: думающие → high, tester/deployer → medium |
|
||||
| `ORCH_AGENT_FALLBACK_MODEL` | опц. фолбэк-модель при overloaded (`--fallback-model`); пусто → без флага |
|
||||
| `ORCH_SELF_DEPLOY_ENABLED` | ORCH-036 kill-switch исполняемого самодеплоя (true); false → legacy-путь для всех |
|
||||
| `ORCH_SELF_DEPLOY_REPOS` | CSV репозиториев с реальным самодеплоем; пусто → только self-hosting `orchestrator` |
|
||||
| `ORCH_DEPLOY_REQUIRE_MANUAL_APPROVE` | требовать человеческий Plane «Approved» для прод-деплоя (true, безопасно) |
|
||||
| `ORCH_DEPLOY_FINALIZE_DELAY_S` / `_MAX_ATTEMPTS` | задержка и бюджет defer'ов finalizer'а (Фаза C; 90 / 10) |
|
||||
| `ORCH_DEPLOY_SSH_USER` / `_SSH_HOST` | куда запускается detached хост-деплой (Фаза B, `ssh user@host`) |
|
||||
| `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` |
|
||||
| `ORCH_RECONCILE_GRACE_DEFAULT_S` | порог «застряла» по `tasks.updated_at`, сек; дефолт `600` |
|
||||
| `ORCH_RECONCILE_GRACE_OVERRIDES_JSON` | per-stage пороги, напр. `{"development":300}`; невалидный JSON → дефолт |
|
||||
| `ORCH_RECONCILE_NOTIFY_UNBLOCK` | слать Telegram при разблокировке застрявшей задачи; дефолт `true` |
|
||||
| `DEPLOY_SSH_USER` / `_HOST` / `DEPLOY_HOOK_SCRIPT` | параметры деплой-хука |
|
||||
|
||||
**Секреты — только в `.env` / `.env.staging` на хосте, в гит НЕ коммитятся.** Канон — `.env.example`, `.env.staging.example`.
|
||||
@@ -96,6 +133,7 @@
|
||||
|
||||
**Страховки:**
|
||||
- Стадия `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` в рамках задачи.
|
||||
|
||||
125
docs/operations/PHANTOM_MERGE_RUNBOOK.md
Normal file
125
docs/operations/PHANTOM_MERGE_RUNBOOK.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Runbook — диагностика «фантомного merge» (ORCH-071)
|
||||
|
||||
> **Когда применять.** Задача дошла до `done` (или прод задеплоен «зелёным»), но есть
|
||||
> подозрение, что её ветка **не влита в `main`** — следующая задача срежет ветку от
|
||||
> устаревшего `main` и потеряет код предшественника (постмортем
|
||||
> `docs/history/LESSONS_2026-06-08_phantom-merge.md`). Этот runbook даёт 4 проверки
|
||||
> для **однозначной локализации** фантома.
|
||||
|
||||
С ORCH-071 такой исход блокируется автоматически: под-гейт `deploy → done`
|
||||
(`stage_engine._handle_merge_verify`) сначала **детерминированно вливает PR**
|
||||
(`merge_gate.merge_pr`, Gitea PR-merge API), затем **верифицирует merge**
|
||||
(`merge_gate.verify_merged_to_main`) и НЕ пускает задачу в `done`, пока merge не
|
||||
подтверждён (alert + HOLD). Этот runbook — для ручной перепроверки/инцидентов
|
||||
(в т.ч. при выключенном kill-switch `ORCH_MERGE_VERIFY_ENABLED=false`).
|
||||
|
||||
Подставьте значения:
|
||||
|
||||
```bash
|
||||
OWNER=admin # settings.gitea_owner
|
||||
REPO=orchestrator # репозиторий
|
||||
BRANCH=feature/ORCH-071-slug # ветка задачи
|
||||
GITEA=http://localhost:3000 # settings.gitea_url
|
||||
TOKEN=<gitea_token> # settings.gitea_token
|
||||
FILE=src/stage_engine.py # любой файл, гарантированно изменённый задачей
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Проверка 1 — Gitea API: список PR + флаги `merged`
|
||||
|
||||
Показывает, считает ли сам Gitea PR влитым.
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: token $TOKEN" \
|
||||
"$GITEA/api/v1/repos/$OWNER/$REPO/pulls?state=all" \
|
||||
| python3 -c 'import sys,json; \
|
||||
[print(p["number"], p["state"], "merged="+str(p.get("merged")), p["head"]["ref"]) \
|
||||
for p in json.load(sys.stdin)]'
|
||||
```
|
||||
|
||||
* **Фантом НЕ подтверждён (всё хорошо):** строка ветки `$BRANCH` имеет `merged=True`.
|
||||
* **Фантом подтверждён (по этому критерию):** PR ветки `state=open` / `merged=False`
|
||||
(или PR отсутствует), при том что задача в `done` / прод задеплоен.
|
||||
|
||||
---
|
||||
|
||||
## Проверка 2 — md5 прод-файлов vs `git show origin/main:<file>`
|
||||
|
||||
Сверяет содержимое файла на проде с тем, что лежит в `origin/main`.
|
||||
|
||||
```bash
|
||||
# в прод-контейнере (или через docker exec orchestrator):
|
||||
md5sum "/app/$FILE"
|
||||
|
||||
# содержимое того же файла из origin/main (на хосте, в клоне репо):
|
||||
git -C /home/slin/repos/$REPO fetch origin main -q
|
||||
git -C /home/slin/repos/$REPO show "origin/main:$FILE" | md5sum
|
||||
```
|
||||
|
||||
* **Совпало:** прод соответствует `main` (фантома нет ИЛИ задача не меняла этот файл —
|
||||
возьмите файл из проверки 3/diff'а ветки).
|
||||
* **Разошлось:** прод собран из ветки, а `main` его не получил → косвенный признак фантома.
|
||||
|
||||
---
|
||||
|
||||
## Проверка 3 — `git merge-base` ветки vs `main`
|
||||
|
||||
Главный детерминированный критерий: является ли HEAD ветки предком `origin/main`.
|
||||
|
||||
```bash
|
||||
git -C /home/slin/repos/$REPO fetch origin -q
|
||||
SHA=$(git -C /home/slin/repos/$REPO rev-parse "origin/$BRANCH")
|
||||
git -C /home/slin/repos/$REPO merge-base --is-ancestor "$SHA" origin/main \
|
||||
&& echo "MERGED: ветка влита в main" \
|
||||
|| echo "NOT MERGED: ветка НЕ предок origin/main (ФАНТОМ)"
|
||||
```
|
||||
|
||||
Это ровно та проверка, что выполняет `merge_gate.verify_merged_to_main` (rc=0 → влито).
|
||||
|
||||
* **`MERGED`:** фантома нет.
|
||||
* **`NOT MERGED`:** фантом подтверждён — `main` не содержит коммитов задачи.
|
||||
|
||||
---
|
||||
|
||||
## Проверка 4 — таймлайн деплой-логов
|
||||
|
||||
Восстанавливает порядок событий: был ли merge до/после деплоя, и был ли он вообще.
|
||||
|
||||
```bash
|
||||
# Вердикт деплоя + новое поле merge-верификации (ORCH-071):
|
||||
git -C /home/slin/repos/$REPO show "origin/$BRANCH:docs/work-items/<WI>/14-deploy-log.md" \
|
||||
| sed -n '1,12p' # frontmatter: deploy_status:, merged_to_main:
|
||||
|
||||
# Наблюдаемость под-гейта в живом сервисе:
|
||||
curl -s "$GITEA_HEALTH/queue" | python3 -c \
|
||||
'import sys,json; print(json.load(sys.stdin)["merge_verify"])'
|
||||
# -> {"enabled":..., "merge_verified_total":..., "not_merged_alerts_total":..., "last_alert_wi":...}
|
||||
|
||||
# Журнал хоста по деплою (sentinel-каталог задачи):
|
||||
ls -la /home/slin/repos/.deploy-state-$REPO/<WI>/
|
||||
cat /home/slin/repos/.deploy-state-$REPO/<WI>/hook.log
|
||||
```
|
||||
|
||||
* `deploy_status: SUCCESS` + `merged_to_main: false` → деплой прошёл, merge — нет
|
||||
(это и есть класс ORCH-071; задача должна быть удержана на `deploy`, не `done`).
|
||||
* `not_merged_alerts_total` растёт / `last_alert_wi == <WI>` → под-гейт уже поднял alert.
|
||||
|
||||
---
|
||||
|
||||
## Критерий «фантом подтверждён»
|
||||
|
||||
Фантомный merge считается **подтверждённым**, если выполняется ХОТЯ БЫ ОДНО из:
|
||||
|
||||
1. Проверка 1: PR ветки `state=open` / `merged=False` (или PR нет), а задача в `done`.
|
||||
2. Проверка 3: `merge-base --is-ancestor` вернул **NOT MERGED** (HEAD ветки не предок `origin/main`).
|
||||
3. Проверка 4: `14-deploy-log.md` имеет `deploy_status: SUCCESS` при `merged_to_main: false`.
|
||||
|
||||
Проверка 2 — вспомогательная (зависит от того, менял ли файл задачей), используется
|
||||
для подтверждения проверок 1/3.
|
||||
|
||||
### Что делать при подтверждённом фантоме
|
||||
|
||||
1. **Влить PR вручную** через Gitea (PR-merge API / UI) — НИКОГДА не `git push`/`--force` в `main` (INV-4).
|
||||
2. Повторить approve задачи (re-drive) — под-гейт переоценит: merge подтвердится → задача уйдёт в `done`.
|
||||
3. Если фантом случился при выключенном kill-switch — включить `ORCH_MERGE_VERIFY_ENABLED=true`.
|
||||
@@ -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.
|
||||
7
docs/work-items/ORCH-022/00-business-request.md
Normal file
7
docs/work-items/ORCH-022/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: [★ высокий] Security-гейт: secret-scanning + аудит зависимостей перед мержем
|
||||
|
||||
Work Item ID: ORCH-022
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
150
docs/work-items/ORCH-022/01-brd.md
Normal file
150
docs/work-items/ORCH-022/01-brd.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# 01 — BRD: Security-гейт (secret-scanning + аудит зависимостей перед мержем)
|
||||
|
||||
Work Item: **ORCH-022**
|
||||
Приоритет: **★ высокий**
|
||||
Источник: предложение Стрим, одобрено Славой (2026-06-04).
|
||||
Стадия: analysis.
|
||||
|
||||
---
|
||||
|
||||
## 1. Бизнес-проблема
|
||||
|
||||
Оркестратор — автономная мульти-агентная система: агенты (`developer`) пишут код
|
||||
**без человека-фильтра по умолчанию**. Перед мержем в `main` сейчас нет проверки на:
|
||||
|
||||
- **утёкший секрет** — закоммиченный API-ключ / токен / пароль / приватный ключ;
|
||||
- **дырявую зависимость** — пакет с известной CVE;
|
||||
- (опционально) **базовую уязвимость кода** — типовой SAST-паттерн.
|
||||
|
||||
Для автономной системы это критично: ошибку, которую в обычной команде «выловили бы
|
||||
глазами на ревью», здесь поймать некому. Утёкший в `git`-историю ключ или уязвимая
|
||||
зависимость может уехать в прод и обслуживать **все** проекты (общий инстанс,
|
||||
self-hosting).
|
||||
|
||||
### Прецеденты / связки
|
||||
- **PR #18** (`check_ci_green`: красный CI → возврат на `development`) — задаёт целевой
|
||||
паттерн поведения красного гейта. Security-гейт должен вести себя так же.
|
||||
- **Управление секретами** (CLAUDE.md §8): секреты живут только в `.env`/`.env.staging`
|
||||
на хосте, канон — `.env.example`. Гейт — это автоматический страж этого правила.
|
||||
|
||||
---
|
||||
|
||||
## 2. Цель
|
||||
|
||||
Ввести **security-гейт перед слиянием ветки задачи в `main`**, который детерминированно
|
||||
(без LLM) проверяет diff/ветку на секреты и уязвимые зависимости и **блокирует
|
||||
продвижение** при нарушении порогов: красный security-гейт → **возврат на `development`**
|
||||
(developer-retry, как красный CI / merge-gate), задача **не уезжает в прод**.
|
||||
|
||||
### Бизнес-ценность
|
||||
- Структурно невозможно «тихо» влить секрет или известную CVE в прод автономной системы.
|
||||
- Самоприменение правила CLAUDE.md §8 (секреты не в гит) без участия человека.
|
||||
- Расширяет уже выстроенную линию автономных страховок (CI-гейт, merge-gate ORCH-043,
|
||||
staging-провенанс ORCH-058, post-deploy ORCH-021).
|
||||
|
||||
---
|
||||
|
||||
## 3. Объём (Scope)
|
||||
|
||||
### 3.1 В объёме (v1) — **предположение по умолчанию (A1)**
|
||||
1. **Secret-scanning** — обязательный минимум гейта. Поиск закоммиченных секретов
|
||||
в ветке задачи / её diff относительно `main`.
|
||||
2. **Dependency audit** — аудит зависимостей проекта на известные CVE.
|
||||
3. **Машиночитаемый артефакт-вердикт** security-гейта (YAML-frontmatter — канон гейтов).
|
||||
4. **Поведение красного гейта** = откат на `development` + developer-retry (cap
|
||||
`MAX_DEVELOPER_RETRIES = 3`), наблюдаемость (Telegram + Plane-коммент).
|
||||
5. **Условный раскат** (kill-switch + scope репозиториев), **never-raise**,
|
||||
self-hosting (`orchestrator`) — первым.
|
||||
|
||||
### 3.2 Вне объёма (v1) — **предположение (A2), отдельные WI**
|
||||
- **SAST (semgrep)** — вынесен в follow-up WI: шумнее, требует policy-тюнинга правил;
|
||||
гейт проектируется с точкой расширения под него, но в v1 не включается.
|
||||
- **Полноценный мульти-стек** (JS/npm, Android) — см. A3 ниже; в v1 целевой стек —
|
||||
Python (сам оркестратор). Связь с ORCH-9/15 фиксируется как зависимость на будущее.
|
||||
- Ретроспективное сканирование уже существующей истории `main` (гейт смотрит вперёд —
|
||||
ветку перед мержем, не чистит прошлое).
|
||||
- Управление аллоулистом ложных срабатываний через UI/Plane (в v1 — файл в репозитории).
|
||||
|
||||
### 3.3 Зафиксированные предположения по умолчанию
|
||||
> ⚠️ Интерактивный опрос Owner на стадии анализа не дал ответа; ниже —
|
||||
> **дефолты по конвенциям проекта**. Любой из них Owner/архитектор может переопределить
|
||||
> (для A4 предусмотрены конфиг-флаги порогов).
|
||||
|
||||
- **A1 (объём сканеров v1):** secret-scanning + dependency-audit. SAST отложен.
|
||||
- **A2 (SAST):** отложен в отдельный WI; гейт оставляет точку расширения.
|
||||
- **A3 (стек):** **Python-only сначала**, реально только для self-hosting
|
||||
(`is_self_hosting_repo` / scope-CSV), как ORCH-35/43/58. Прочие репо — no-op pass.
|
||||
Мульти-стек (детект стека по репо) — отдельный WI.
|
||||
- **A4 (пороги):** **секреты — всегда блок**; **зависимости — блок на HIGH/CRITICAL,
|
||||
warning на MEDIUM/LOW**. Пороги вынесены в конфиг (переопределяемы без редеплоя кода).
|
||||
|
||||
---
|
||||
|
||||
## 4. Заинтересованные стороны
|
||||
| Роль | Интерес |
|
||||
|------|---------|
|
||||
| Owner (Слава) | Прод-безопасность автономного конвейера; контроль порогов и раската. |
|
||||
| Стрим | Инициатор; снижение риска утечки/уязвимости в автономном режиме. |
|
||||
| Агент `developer` | Получает понятную причину красного гейта → быстрый фикс. |
|
||||
| Агент `reviewer` | Гейт снимает с него непосильную задачу «глазами ловить ключи». |
|
||||
| Все проекты на инстансе | Общий прод не должен получить секрет/CVE через одну задачу. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Бизнес-требования
|
||||
|
||||
| ID | Требование | Приоритет |
|
||||
|----|-----------|-----------|
|
||||
| BR-1 | Перед слиянием ветки задачи в `main` обязателен security-гейт (секреты + аудит зависимостей). | MUST |
|
||||
| BR-2 | Найден секрет (порог A4) → гейт **красный** → откат на `development`, в прод не уходит. | MUST |
|
||||
| BR-3 | Уязвимость зависимости уровня блокировки (порог A4) → гейт **красный** → откат на `development`. | MUST |
|
||||
| BR-4 | Уязвимость ниже порога блокировки → **warning**, продвижение не блокируется, но фиксируется в артефакте. | MUST |
|
||||
| BR-5 | Красный гейт ведёт себя как красный CI / merge-gate: откат на `development` + developer-retry (cap 3), затем эскалация (Telegram + Plane Blocked). | MUST |
|
||||
| BR-6 | Вердикт гейта — **машиночитаемый** (YAML-frontmatter артефакта), читается гейтом ТОЛЬКО из frontmatter (канон проекта), не из прозы. | MUST |
|
||||
| BR-7 | Гейт **детерминированный, без LLM** в критическом пути (как merge-gate / image-freshness). | MUST |
|
||||
| BR-8 | Гейт **never-raise**: внутренняя ошибка не роняет `advance_stage` и не вешает конвейер всех проектов. | MUST |
|
||||
| BR-9 | Условный раскат: глобальный kill-switch + scope-CSV репозиториев; пусто → реально только self-hosting (`orchestrator`), прочие репо — no-op pass. | MUST |
|
||||
| BR-10 | Пороги блокировки конфигурируемы (env-флаги, без редеплоя кода). | SHOULD |
|
||||
| BR-11 | Наблюдаемость: причина блокировки видна (Telegram + Plane-коммент + артефакт); проход — без шума. | MUST |
|
||||
| BR-12 | Документация (CLAUDE.md «Артефакты задачи», `docs/architecture/README.md` таблица гейтов, CHANGELOG, ADR) обновлена в том же PR. | MUST |
|
||||
| BR-13 | Аллоулист ложных срабатываний (заведомо-безопасные совпадения, напр. в `.env.example`, фикстуры тестов) поддерживается версионируемым файлом в репозитории. | SHOULD |
|
||||
| BR-14 | Точка расширения под SAST и мульти-стек заложена, но в v1 не активна (A2/A3). | SHOULD |
|
||||
|
||||
---
|
||||
|
||||
## 6. Ограничения и риски (бизнес-уровень)
|
||||
- **Self-hosting:** гейт исполняется внутри инстанса, который правит сам себя. Запрет на
|
||||
рестарт/падение прод-контейнера в рамках задачи (CLAUDE.md §self-hosting) сохраняется —
|
||||
гейт ничего не деплоит и не рестартит, только читает/сканирует.
|
||||
- **Ложные срабатывания** (false positives) могут зациклить откат `→ development`
|
||||
(прецедент ORCH-061 со staging-петлёй). Митигировано: cap retry=3 + аллоулист (BR-13)
|
||||
+ конфигурируемые пороги (BR-10) + kill-switch (BR-9).
|
||||
- **Внешние БД уязвимостей** (CVE-фиды) — сетевая зависимость; недоступность фида не
|
||||
должна давать ложный красный (см. AC: degrade-поведение при недоступности фида —
|
||||
решение порога «fail-open vs fail-closed для аудита» закрепляется в acceptance + ADR).
|
||||
- **Стоимость/время** сканирования добавляется к каждому прогону задачи — должно быть
|
||||
ограничено таймаутом (как merge-retest).
|
||||
|
||||
---
|
||||
|
||||
## 7. Критерий успеха (бизнес)
|
||||
Ветка с подсаженным тестовым секретом и/или зависимостью с известной CRITICAL-CVE
|
||||
**не может** дойти до `main`/прода: гейт краснеет, задача откатывается на `development`
|
||||
с понятной причиной. Чистая ветка проходит гейт без задержек и без шума. Для не-self
|
||||
репозиториев конвейер не меняется (no-op). Прод-контейнер не рестартится гейтом.
|
||||
|
||||
---
|
||||
|
||||
## 8. Открытые вопросы (для архитектора / Owner)
|
||||
1. **Размещение гейта** (решение архитектора): (а) на стадии `review`, либо (б) отдельный
|
||||
под-гейт перед мержем на ребре `deploy-staging → deploy` (где уже живёт merge-gate
|
||||
ORCH-043 / image-freshness ORCH-058). Требование BRD — «перед слиянием в `main`»;
|
||||
обе опции его удовлетворяют. См. 02-trz §4.
|
||||
2. **Где запускается сканер**: новый job в `.gitea/workflows/ci.yml` (тогда вердикт может
|
||||
течь через существующий `check_ci_green`) **или** отдельный QG-чек/под-гейт в `src/qg`.
|
||||
Решение — архитектор (02-trz фиксирует требования к обоим путям).
|
||||
3. **Аудит зависимостей при недоступном CVE-фиде:** fail-open (warning) или fail-closed
|
||||
(блок)? Дефолт-предложение — **fail-open с громким warning** (не плодить ложные
|
||||
завороты), закрепить в ADR.
|
||||
4. **Выбор конкретных инструментов** (gitleaks vs trufflehog; pip-audit vs trivy) —
|
||||
технологическое решение архитектора; BRD фиксирует только функцию.
|
||||
175
docs/work-items/ORCH-022/02-trz.md
Normal file
175
docs/work-items/ORCH-022/02-trz.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# 02 — ТЗ: Security-гейт (secret-scanning + dependency audit)
|
||||
|
||||
Work Item: **ORCH-022** · Стадия: analysis · См. `01-brd.md`, `03-acceptance-criteria.md`.
|
||||
|
||||
> **Граница ответственности аналитика.** Ниже — *функциональные требования и точки
|
||||
> касания* кода. Выбор размещения гейта в пайплайне, конкретных инструментов и схемы
|
||||
> модулей — **решение архитектора** (см. §4 и `01-brd.md` §8). ТЗ фиксирует требования к
|
||||
> любому из допустимых вариантов и инварианты, которые нельзя нарушать.
|
||||
|
||||
---
|
||||
|
||||
## 1. Контекст кода (как есть)
|
||||
|
||||
- **Стадии:** `src/stages.py::STAGE_TRANSITIONS` — линейный конвейер
|
||||
`… review → testing → deploy-staging → deploy → done`. Фактический merge ветки в
|
||||
`main` делает агент `deployer` **в начале стадии `deploy`** (CLAUDE/README).
|
||||
- **Quality Gates:** `src/qg/checks.py` — реестр `QG_CHECKS` (имя → функция), сигнатуры
|
||||
диспетчеризуются в `src/stage_engine.py::_run_qg`.
|
||||
- **Существующий паттерн «красный гейт → возврат developer»:**
|
||||
`check_ci_green` (PR #18) и rollback-ветки в
|
||||
`stage_engine._handle_qg_failure_rollbacks` (откат на `development`, developer-retry,
|
||||
cap `MAX_DEVELOPER_RETRIES = 3`, затем `set_issue_blocked` + Telegram).
|
||||
- **Эталонный паттерн детерминированного под-гейта на ребре** (без LLM, never-raise,
|
||||
условный раскат, откат на `development`):
|
||||
- merge-gate **ORCH-043** — `src/merge_gate.py` + `check_branch_mergeable` +
|
||||
`stage_engine._handle_merge_gate` (ребро `deploy-staging → deploy`);
|
||||
- image-freshness **ORCH-058** — `src/image_freshness.py` + `_check_staging_image_fresh`
|
||||
+ `stage_engine._handle_image_freshness` (то же ребро).
|
||||
Оба: leaf-модуль с чистой логикой (never-raise) + тонкая обёртка в `QG_CHECKS` +
|
||||
врезка-обработчик в `advance_stage`, kill-switch `*_enabled` + scope `*_repos`,
|
||||
реально только для self-hosting при пустом scope.
|
||||
- **CI:** `.gitea/workflows/ci.yml` — один job `test` (pytest) на `self-hosted` раннере,
|
||||
push в `feature/**` и PR в `main`. `check_ci_green` читает комбинированный статус
|
||||
коммита из Gitea API.
|
||||
- **Артефакты задачи** нумерованы до `16-post-deploy-log.md`.
|
||||
- **Зависимости Python:** `requirements.txt` (корень репо).
|
||||
|
||||
---
|
||||
|
||||
## 2. Функциональные требования к реализации
|
||||
|
||||
### FR-1. Secret-scanning ветки перед мержем
|
||||
- Сканировать ветку задачи / её diff относительно `origin/main` на секреты
|
||||
(ключи, токены, пароли, приватные ключи).
|
||||
- **Любой** подтверждённый секрет (не из аллоулиста) → вердикт **FAIL** (порог A4: секреты
|
||||
всегда блокируют).
|
||||
- Инструмент (gitleaks / trufflehog) — выбор архитектора. Должен запускаться offline-/
|
||||
детерминированно (без LLM) и иметь конфиг правил/аллоулиста в репозитории.
|
||||
|
||||
### FR-2. Dependency audit
|
||||
- Аудит зависимостей целевого стека на известные CVE. Для Python — манифест
|
||||
`requirements.txt` (инструмент pip-audit / trivy — выбор архитектора).
|
||||
- Классификация по severity. **Порог блокировки (A4, конфигурируемо BR-10):**
|
||||
- `CRITICAL`, `HIGH` → вклад в **FAIL**;
|
||||
- `MEDIUM`, `LOW` → **warning** (фиксируется в артефакте, не блокирует).
|
||||
- Недоступность CVE-фида: degrade-поведение по решению ADR (дефолт-предложение —
|
||||
fail-open + громкий warning, чтобы не плодить ложные завороты). Поведение должно быть
|
||||
детерминированным и протестированным.
|
||||
|
||||
### FR-3. Машиночитаемый артефакт-вердикт
|
||||
- Гейт порождает артефакт security-отчёта с **YAML-frontmatter**, напр.:
|
||||
```
|
||||
---
|
||||
security_status: PASS # PASS | FAIL
|
||||
secrets_found: 0
|
||||
deps_blocking: 0 # число уязвимостей уровня блокировки
|
||||
deps_warning: 2
|
||||
---
|
||||
```
|
||||
Имя артефакта — предложение: **`17-security-report.md`** (следующий свободный номер;
|
||||
финализирует архитектор). Тело — человекочитаемый список находок.
|
||||
- Вердикт читается гейтом **ТОЛЬКО из frontmatter** (канон проекта: «машинные вердикты —
|
||||
строго YAML-frontmatter, никогда проза»), по образцу `_parse_deploy_status` /
|
||||
`_parse_staging_status` / `check_reviewer_verdict`. Negative-токен (FAIL) авторитетен.
|
||||
- Отсутствие/битый frontmatter → `(False, reason)` (fail-closed на чтении вердикта,
|
||||
как у существующих парсеров).
|
||||
|
||||
### FR-4. Поведение красного гейта (откат)
|
||||
- `security_status: FAIL` → откат на `development` + enqueue `developer`, по образцу
|
||||
`_handle_qg_failure_rollbacks` (merge-gate-ветка — точный шаблон):
|
||||
- cap `MAX_DEVELOPER_RETRIES` (3); при исчерпании — `set_issue_blocked` + Telegram-алерт;
|
||||
- `task_desc` для developer несёт **дословную причину** (какие секреты/CVE), по образцу
|
||||
ORCH-046 (встраивание must-fix в `task_desc`), а не только ссылку на артефакт;
|
||||
- Plane-коммент + `notify_qg_failure` (наблюдаемость BR-11).
|
||||
|
||||
### FR-5. Условный раскат (как ORCH-35/43/58)
|
||||
- Глобальный kill-switch `security_gate_enabled` (env `ORCH_SECURITY_GATE_ENABLED`,
|
||||
дефолт по согласованию; рекомендуется `true` с safety-net, как у соседних фич).
|
||||
- Scope `security_gate_repos` (CSV); пусто → реально только `is_self_hosting_repo(repo)`
|
||||
(`orchestrator`). Прочие репо → `(True, "security-gate N/A for <repo>")` (мгновенный pass).
|
||||
- Отдельные пороги-флаги (A4/BR-10): напр. `security_dep_block_severity`
|
||||
(`HIGH` по умолчанию), при желании `security_secrets_block` (`true`).
|
||||
|
||||
### FR-6. never-raise
|
||||
- Любая внутренняя ошибка гейта (сбой сканера, отсутствие бинаря, таймаут) →
|
||||
`(False, "<reason>")` **без** проброса исключения в `advance_stage`. Контракт —
|
||||
как у `check_branch_mergeable` (внешний + внутренний guard).
|
||||
- Таймаут сканирования ограничен (по образцу `merge_retest_timeout_s`).
|
||||
|
||||
### FR-7. Наблюдаемость
|
||||
- Блокировка → Telegram + Plane-коммент (BR-11). Проход → лог-строка, без шумных
|
||||
нотификаций (по образцу merge-gate pass).
|
||||
- Желательно: краткий снимок в `GET /queue` (опционально, по образцу блоков `reconcile`/
|
||||
`reaper`/`post_deploy`) — на усмотрение архитектора.
|
||||
|
||||
---
|
||||
|
||||
## 3. Задействованные модули `src/` (точки касания)
|
||||
|
||||
| Модуль | Изменение |
|
||||
|--------|-----------|
|
||||
| `src/security_gate.py` (**новый leaf-модуль**) | Чистая логика гейта: запуск сканеров, классификация по severity, применение порогов/аллоулиста, формирование вердикта + парсер frontmatter. **never-raise.** По образцу `src/merge_gate.py` / `src/image_freshness.py` / `src/post_deploy.py`. |
|
||||
| `src/qg/checks.py` | Новый чек `check_security_gate` (тонкая обёртка над `security_gate`, ленивый импорт во избежание циклов) + регистрация в `QG_CHECKS`. Условность (kill-switch/scope/self-hosting) — как `check_branch_mergeable` / `_check_staging_image_fresh`. |
|
||||
| `src/stage_engine.py` | Врезка-обработчик `_handle_security_gate(...)` по образцу `_handle_merge_gate` / `_handle_image_freshness`: вызов в `advance_stage` на выбранном архитектором ребре; FAIL → откат на `development` (FR-4); never-raise. **`STAGE_TRANSITIONS` НЕ меняется**, если выбран вариант «под-гейт ребра». |
|
||||
| `src/config.py` | Новые настройки: `security_gate_enabled`, `security_gate_repos`, `security_dep_block_severity`, `security_scan_timeout_s` (+ при необходимости пути к бинарям/конфигам сканеров). С docstring-комментариями по образцу ORCH-043/058. |
|
||||
| `.gitea/workflows/ci.yml` | **Если** архитектор выберет CI-путь: новый job `security` (secret-scan + dep-audit), влияющий на комбинированный статус коммита (тогда срабатывает `check_ci_green`-паттерн PR #18). Иначе — не трогается. |
|
||||
| `requirements.txt` / Dockerfile | Установка выбранных сканеров (если они Python-пакеты — в `requirements.txt`; если бинари — в Dockerfile/раннер). |
|
||||
| Конфиг сканера + аллоулист | Версионируемые файлы в репозитории (напр. `.gitleaks.toml` / аллоулист) — BR-13. |
|
||||
| `.openclaw/agents/developer.md` | (Если нужно) краткая инструкция developer'у про устранение security-находок при заворотах. |
|
||||
|
||||
> Если выбран вариант «гейт на стадии `review`» — врезка делается в соответствующую
|
||||
> ветку `advance_stage`/обработчик ревью вместо ребра `deploy-staging → deploy`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Размещение в пайплайне — варианты для архитектора
|
||||
|
||||
Требование BRD: **«перед слиянием ветки в `main`»**. Допустимы (выбор + обоснование — в ADR):
|
||||
|
||||
- **Вариант R (review):** security-проверка на стадии `review` (раньше отлов, дешевле
|
||||
откат — задача ещё близко к development). Минус: дальше по конвейеру `main` может уйти
|
||||
вперёд (но это закрывает merge-gate).
|
||||
- **Вариант M (merge-edge, рекомендуемый к рассмотрению):** под-гейт на ребре
|
||||
`deploy-staging → deploy`, рядом с merge-gate (ORCH-043) и image-freshness (ORCH-058) —
|
||||
непосредственно перед фактическим мержем `deployer`'ом. Плюс: единое место «последней
|
||||
страховки перед main», переиспользование готового паттерна врезки/отката/lease.
|
||||
- **Вариант C (CI-job):** добавить job в `ci.yml`; вердикт течёт через `check_ci_green`.
|
||||
Плюс: меньше нового кода в движке. Минус: пороги/severity-логика и артефакт-вердикт
|
||||
сложнее выразить только статусом коммита.
|
||||
|
||||
ТЗ не предписывает вариант; реализация обязана сохранить инварианты §6.
|
||||
|
||||
---
|
||||
|
||||
## 5. Изменения API
|
||||
- Новых HTTP-endpoint'ов **не требуется**.
|
||||
- Допустимо (опционально, FR-7): расширить ответ `GET /queue` блоком `security`
|
||||
(counts/last_run) — по образцу блоков `reconcile`/`reaper`/`post_deploy`. Не обязательно.
|
||||
|
||||
## 6. Изменения схемы БД
|
||||
- **Не требуется.** Состояние гейта — артефакт-файл + (при необходимости) sentinel-файлы,
|
||||
по образцу merge-lease / deploy-state / post-deploy-state. Миграций БД нет.
|
||||
- Если архитектор сочтёт нужным считать security-retry отдельно от developer-retry —
|
||||
предпочесть подсчёт по `jobs`/`agent_runs` (как `_developer_retry_count` /
|
||||
`_merge_defer_count`), без новых колонок.
|
||||
|
||||
## 7. Инварианты (НЕ нарушать)
|
||||
1. `STAGE_TRANSITIONS` и реестр `QG_CHECKS` остаются консистентными; при варианте
|
||||
«под-гейт ребра» — `STAGE_TRANSITIONS` не меняется (триггер — то же событие стадии).
|
||||
2. Машинный вердикт — только из YAML-frontmatter, не из прозы.
|
||||
3. never-raise: гейт никогда не пробрасывает исключение в `advance_stage`.
|
||||
4. Условность как ORCH-35/43/58: не-self репо при пустом scope не затрагиваются (no-op).
|
||||
5. Гейт **не деплоит и не рестартит** прод-контейнер (self-hosting safety).
|
||||
6. Откат и retry-счётчик developer не ломаются (cap=3, затем эскалация).
|
||||
7. Документация (CLAUDE.md, README, CHANGELOG, ADR) обновлена в том же PR (BR-12).
|
||||
|
||||
## 8. Артефакты pipeline, создаваемые/обновляемые
|
||||
- **Новый:** `docs/work-items/ORCH-022/17-security-report.md` (имя финализирует архитектор)
|
||||
с `security_status:`-frontmatter (FR-3) — порождается гейтом per-task.
|
||||
- **ADR:** `docs/work-items/ORCH-022/06-adr/ADR-001-<slug>.md` (решение: размещение,
|
||||
инструменты, degrade-поведение фида, пороги). При сквозном влиянии — global ADR в
|
||||
`docs/architecture/adr/`.
|
||||
- **Обновить:** `CLAUDE.md` (раздел «Артефакты задачи» — добавить 17-…),
|
||||
`docs/architecture/README.md` (таблица гейтов + реестр `QG_CHECKS` + новый раздел),
|
||||
`CHANGELOG.md`, `.env.example` (новые `ORCH_SECURITY_*`).
|
||||
140
docs/work-items/ORCH-022/03-acceptance-criteria.md
Normal file
140
docs/work-items/ORCH-022/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# 03 — Критерии приёмки: Security-гейт (ORCH-022)
|
||||
|
||||
Формат: каждый критерий имеет чёткое условие **PASS/FAIL**. Привязка к
|
||||
`01-brd.md` (BR-*) и `02-trz.md` (FR-*).
|
||||
|
||||
---
|
||||
|
||||
## A. Secret-scanning (FR-1, BR-1/BR-2)
|
||||
|
||||
### AC-1 — Подсаженный секрет блокирует гейт
|
||||
- **PASS:** ветка с тестовым секретом (напр. фиктивный AWS-ключ формата `AKIA…` вне
|
||||
аллоулиста) → `security_status: FAIL`; гейт возвращает `(False, reason)`, причина
|
||||
называет секрет/файл.
|
||||
- **FAIL:** секрет не обнаружен ИЛИ гейт зелёный при наличии секрета.
|
||||
|
||||
### AC-2 — Чистая ветка проходит
|
||||
- **PASS:** ветка без секретов → `security_status: PASS`; `secrets_found: 0`;
|
||||
гейт возвращает `(True, …)`.
|
||||
- **FAIL:** ложное срабатывание (FAIL на чистой ветке).
|
||||
|
||||
### AC-3 — Аллоулист подавляет заведомо-безопасное (BR-13)
|
||||
- **PASS:** совпадение, явно занесённое в версионируемый аллоулист (напр. плейсхолдер в
|
||||
`.env.example` / фикстура теста), **не** даёт FAIL.
|
||||
- **FAIL:** аллоулист игнорируется и даёт ложный FAIL.
|
||||
|
||||
---
|
||||
|
||||
## B. Dependency audit (FR-2, BR-3/BR-4)
|
||||
|
||||
### AC-4 — CVE уровня блокировки краснит гейт
|
||||
- **PASS:** зависимость с известной `CRITICAL`/`HIGH` CVE (при пороге по умолчанию
|
||||
`HIGH`) → вклад в `security_status: FAIL`; `deps_blocking >= 1`.
|
||||
- **FAIL:** блокирующая уязвимость не приводит к FAIL.
|
||||
|
||||
### AC-5 — Низкая severity = warning, не блок
|
||||
- **PASS:** только `MEDIUM`/`LOW` уязвимости → `security_status: PASS`, при этом
|
||||
`deps_warning >= 1` и находки перечислены в теле артефакта.
|
||||
- **FAIL:** `MEDIUM`/`LOW` блокирует продвижение.
|
||||
|
||||
### AC-6 — Порог блокировки конфигурируем (BR-10)
|
||||
- **PASS:** при `ORCH_SECURITY_DEP_BLOCK_SEVERITY=CRITICAL` та же `HIGH`-уязвимость
|
||||
становится warning (не блок); при `=HIGH` — блок. Поведение детерминированно
|
||||
определяется флагом.
|
||||
- **FAIL:** флаг не влияет на классификацию.
|
||||
|
||||
### AC-7 — Degrade при недоступном CVE-фиде
|
||||
- **PASS:** недоступность фида обрабатывается по решению ADR детерминированно и
|
||||
протестированно (дефолт: fail-open + громкий warning, гейт не краснеет ложно).
|
||||
- **FAIL:** недоступность фида даёт неконтролируемый красный/исключение.
|
||||
|
||||
---
|
||||
|
||||
## C. Вердикт и артефакт (FR-3, BR-6)
|
||||
|
||||
### AC-8 — Машинный вердикт только из frontmatter
|
||||
- **PASS:** вердикт читается ТОЛЬКО из YAML-frontmatter `17-security-report.md`; проза с
|
||||
«PASS»/«FAIL» в теле не влияет на решение. Negative-токен (FAIL) авторитетен.
|
||||
- **FAIL:** вердикт извлекается из тела/прозы.
|
||||
|
||||
### AC-9 — Битый/отсутствующий frontmatter → fail-closed на чтении
|
||||
- **PASS:** нет frontmatter / битый YAML / нет поля `security_status` → `(False, reason)`
|
||||
(как `_parse_deploy_status`/`check_reviewer_verdict`).
|
||||
- **FAIL:** битый артефакт трактуется как PASS.
|
||||
|
||||
### AC-10 — Артефакт создаётся с корректными полями
|
||||
- **PASS:** после прогона существует `17-security-report.md` с валидным frontmatter
|
||||
(`security_status`, `secrets_found`, `deps_blocking`, `deps_warning`) и телом-списком.
|
||||
- **FAIL:** артефакт не создан/без машинных полей.
|
||||
|
||||
---
|
||||
|
||||
## D. Откат и retry (FR-4, BR-5)
|
||||
|
||||
### AC-11 — Красный гейт → откат на development + developer-retry
|
||||
- **PASS:** `FAIL` → стадия задачи становится `development`, enqueue `developer`,
|
||||
Plane-коммент + `notify_qg_failure`; счётчик developer-retry растёт.
|
||||
- **FAIL:** при FAIL задача продвигается дальше / не откатывается.
|
||||
|
||||
### AC-12 — task_desc несёт дословную причину (ORCH-046-паттерн)
|
||||
- **PASS:** `task_desc` для перезапущенного developer содержит конкретику находок
|
||||
(какие секреты/CVE), а не только ссылку на артефакт.
|
||||
- **FAIL:** developer получает только ссылку без сути.
|
||||
|
||||
### AC-13 — Cap retry и эскалация
|
||||
- **PASS:** после `MAX_DEVELOPER_RETRIES` (3) безуспешных фиксов — `set_issue_blocked` +
|
||||
Telegram-алерт; бесконечного отскока нет.
|
||||
- **FAIL:** откат зацикливается без cap/эскалации.
|
||||
|
||||
---
|
||||
|
||||
## E. Условный раскат и устойчивость (FR-5/FR-6, BR-8/BR-9)
|
||||
|
||||
### AC-14 — Не-self репозиторий = no-op pass
|
||||
- **PASS:** для repo, не входящего в scope и не self-hosting → гейт возвращает
|
||||
`(True, "security-gate N/A for <repo>")` мгновенно, конвейер такого репо не меняется.
|
||||
- **FAIL:** гейт реально запускается/блокирует чужой репо при пустом scope.
|
||||
|
||||
### AC-15 — Kill-switch отключает гейт
|
||||
- **PASS:** `ORCH_SECURITY_GATE_ENABLED=false` → гейт — no-op pass (`(True, …)`),
|
||||
поведение конвейера 1:1 как до ORCH-022.
|
||||
- **FAIL:** при выключенном флаге гейт всё ещё блокирует.
|
||||
|
||||
### AC-16 — never-raise
|
||||
- **PASS:** искусственный сбой (нет бинаря сканера / таймаут / исключение внутри) →
|
||||
`(False, reason)` без проброса исключения; `advance_stage` не падает, конвейер других
|
||||
задач/проектов не встаёт.
|
||||
- **FAIL:** внутренняя ошибка пробрасывается/вешает движок.
|
||||
|
||||
### AC-17 — Таймаут ограничен
|
||||
- **PASS:** сканирование, превысившее `ORCH_SECURITY_SCAN_TIMEOUT_S`, корректно
|
||||
прерывается → детерминированный вердикт (по политике degrade), без зависания.
|
||||
- **FAIL:** сканер висит без таймаута.
|
||||
|
||||
---
|
||||
|
||||
## F. Инварианты и интеграция (BR-7/BR-12, TRZ §7)
|
||||
|
||||
### AC-18 — STAGE_TRANSITIONS/QG_CHECKS консистентны
|
||||
- **PASS:** при варианте «под-гейт ребра» `STAGE_TRANSITIONS` не изменён; новый чек
|
||||
зарегистрирован в `QG_CHECKS`; `_run_qg` корректно его диспетчеризует. Все
|
||||
существующие тесты гейтов/стадий зелёные.
|
||||
- **FAIL:** сломан реестр/переходы/существующие тесты.
|
||||
|
||||
### AC-19 — Гейт не деплоит/не рестартит прод
|
||||
- **PASS:** код гейта не вызывает деплой-хук/рестарт прод-контейнера; только
|
||||
чтение/сканирование.
|
||||
- **FAIL:** гейт инициирует рестарт/деплой.
|
||||
|
||||
### AC-20 — Документация обновлена в том же PR (BR-12)
|
||||
- **PASS:** обновлены `CLAUDE.md` (артефакт 17-…), `docs/architecture/README.md`
|
||||
(таблица гейтов + реестр QG + раздел ORCH-022), `CHANGELOG.md`, `.env.example`
|
||||
(`ORCH_SECURITY_*`); заведён ADR `06-adr/ADR-001-*`.
|
||||
- **FAIL:** функционал есть, документация/ADR не обновлены → reviewer обязан
|
||||
REQUEST_CHANGES (CLAUDE.md §6).
|
||||
|
||||
### AC-21 — End-to-end на тестовой задаче
|
||||
- **PASS:** прогон на self-hosting-репо: грязная ветка (секрет/CVE) → откат на
|
||||
`development`; после фикса чистая ветка → гейт зелёный → конвейер идёт дальше; прод не
|
||||
затронут в процессе.
|
||||
- **FAIL:** любой шаг E2E не воспроизводится.
|
||||
126
docs/work-items/ORCH-022/04-test-plan.yaml
Normal file
126
docs/work-items/ORCH-022/04-test-plan.yaml
Normal file
@@ -0,0 +1,126 @@
|
||||
work_item: ORCH-022
|
||||
title: "Security-гейт: secret-scanning + dependency audit перед мержем"
|
||||
notes: >
|
||||
План тестов для security-гейта. Чистая логика выносится в leaf-модуль
|
||||
src/security_gate.py (never-raise) — основной предмет unit-тестов (по образцу
|
||||
tests для merge_gate / image_freshness / post_deploy / staging_verdict).
|
||||
Интеграция врезки в advance_stage и условный раскат — integration-тесты.
|
||||
Имена модулей тестов финализирует разработчик/архитектор по факту реализации.
|
||||
|
||||
tests:
|
||||
# --- Secret-scanning (FR-1 / AC-1..AC-3) ---
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "Подсаженный тестовый секрет в diff -> вердикт FAIL, secrets_found>=1, причина называет находку."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: "Чистая ветка без секретов -> вердикт PASS, secrets_found=0."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: "Совпадение из аллоулиста (плейсхолдер .env.example / фикстура) НЕ даёт FAIL."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
|
||||
# --- Dependency audit + пороги (FR-2 / AC-4..AC-7) ---
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: "CVE уровня HIGH/CRITICAL при пороге HIGH -> вклад в FAIL, deps_blocking>=1."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: "Только MEDIUM/LOW уязвимости -> PASS, deps_warning>=1, находки в теле артефакта."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: "Конфиг порога: severity=CRITICAL делает HIGH-CVE warning; severity=HIGH делает её блоком."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: "Недоступный CVE-фид -> детерминированный degrade по политике ADR (дефолт fail-open + warning), без исключения и без ложного FAIL."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
|
||||
# --- Вердикт / парсер frontmatter (FR-3 / AC-8..AC-10) ---
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: "Вердикт читается ТОЛЬКО из YAML-frontmatter; проза PASS/FAIL в теле не влияет; negative-токен авторитетен."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: "Нет frontmatter / битый YAML / нет поля security_status -> (False, reason) (fail-closed на чтении)."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: "Артефакт 17-security-report.md создаётся с валидным frontmatter (security_status, secrets_found, deps_blocking, deps_warning) и телом-списком."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
|
||||
# --- never-raise / таймаут / условность (FR-5/FR-6 / AC-14..AC-17) ---
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: "Отсутствие бинаря сканера / внутреннее исключение -> (False, reason), исключение не пробрасывается (never-raise)."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: "Превышение ORCH_SECURITY_SCAN_TIMEOUT_S -> корректное прерывание и детерминированный вердикт, без зависания."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-13
|
||||
type: unit
|
||||
description: "check_security_gate: не-self репо при пустом scope -> (True, 'security-gate N/A for <repo>') мгновенно."
|
||||
module: tests/test_qg_security.py
|
||||
expected: PASS
|
||||
- id: TC-14
|
||||
type: unit
|
||||
description: "check_security_gate: ORCH_SECURITY_GATE_ENABLED=false -> no-op pass (True)."
|
||||
module: tests/test_qg_security.py
|
||||
expected: PASS
|
||||
- id: TC-15
|
||||
type: unit
|
||||
description: "Новый чек зарегистрирован в QG_CHECKS и корректно диспетчеризуется _run_qg."
|
||||
module: tests/test_qg_security.py
|
||||
expected: PASS
|
||||
|
||||
# --- Откат / retry в stage_engine (FR-4 / AC-11..AC-13) ---
|
||||
- id: TC-16
|
||||
type: integration
|
||||
description: "security_status FAIL -> advance_stage откатывает на development, enqueue developer, Plane-коммент + notify_qg_failure."
|
||||
module: tests/test_stage_engine_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-17
|
||||
type: integration
|
||||
description: "task_desc перезапущенного developer содержит дословную причину находок (ORCH-046-паттерн), не только ссылку."
|
||||
module: tests/test_stage_engine_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-18
|
||||
type: integration
|
||||
description: "После MAX_DEVELOPER_RETRIES (3) -> set_issue_blocked + Telegram-алерт; бесконечного отскока нет."
|
||||
module: tests/test_stage_engine_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-19
|
||||
type: integration
|
||||
description: "security_status PASS -> advance_stage продвигает конвейер штатно (без отката, без шумных нотификаций)."
|
||||
module: tests/test_stage_engine_security_gate.py
|
||||
expected: PASS
|
||||
|
||||
# --- Инварианты / интеграция (BR-7/BR-12 / AC-18..AC-19) ---
|
||||
- id: TC-20
|
||||
type: integration
|
||||
description: "При варианте 'под-гейт ребра' STAGE_TRANSITIONS не изменён; существующие тесты стадий/гейтов остаются зелёными."
|
||||
module: tests/test_stages.py
|
||||
expected: PASS
|
||||
- id: TC-21
|
||||
type: integration
|
||||
description: "Гейт не вызывает деплой-хук/рестарт прод-контейнера (self-hosting safety)."
|
||||
module: tests/test_stage_engine_security_gate.py
|
||||
expected: PASS
|
||||
235
docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md
Normal file
235
docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# ADR-001: Security-гейт — secret-scanning + dependency audit перед мержем
|
||||
|
||||
- **Статус:** Accepted (proposed → принято архитектором ORCH-022)
|
||||
- **Дата:** 2026-06-07
|
||||
- **Задача:** ORCH-022
|
||||
- **Связанный global ADR:** `docs/architecture/adr/adr-0012-security-gate.md`
|
||||
- **Источники:** `01-brd.md` (BR-1..BR-14), `02-trz.md` (FR-1..FR-7, §4 варианты, §7 инварианты),
|
||||
`03-acceptance-criteria.md` (AC-1..AC-21).
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
Оркестратор автономен: `developer`-агент пишет код без человека-фильтра. Перед слиянием
|
||||
ветки задачи в `main` нет автоматической проверки на утёкший секрет (ключ/токен/пароль/
|
||||
приватный ключ) и на уязвимую зависимость (известная CVE). Для self-hosting это особенно
|
||||
опасно: один общий прод-инстанс обслуживает все проекты с общей БД — секрет или CVE,
|
||||
просочившийся через одну задачу, попадает в прод всех проектов (CLAUDE.md §self-hosting, §8).
|
||||
|
||||
Конвейер уже содержит линию детерминированных страховок на ребре `deploy-staging → deploy`
|
||||
(непосредственно перед фактическим мержем PR в `main`, который делает `deployer` в начале
|
||||
стадии `deploy`):
|
||||
|
||||
- **merge-gate** (ORCH-043, `check_branch_mergeable`) — догон `main` + re-test + сериализация;
|
||||
- **image-freshness** (ORCH-058, `check_staging_image_fresh`) — провенанс staging-образа.
|
||||
|
||||
Оба построены по одному паттерну: **leaf-модуль чистой логики (never-raise) + тонкая обёртка
|
||||
в `QG_CHECKS` + врезка-обработчик `_handle_*` в `advance_stage`**, с условным раскатом
|
||||
(`*_enabled` + `*_repos`, реально только для self-hosting при пустом scope) и откатом на
|
||||
`development` с developer-retry (cap `MAX_DEVELOPER_RETRIES = 3`).
|
||||
|
||||
Открытые вопросы BRD §8 / TRZ §4, требующие решения архитектора:
|
||||
1. Размещение гейта в пайплайне (review / merge-edge / CI-job).
|
||||
2. Где запускается сканер (CI-job через `check_ci_green` / отдельный QG-чек).
|
||||
3. Degrade при недоступном CVE-фиде (fail-open / fail-closed).
|
||||
4. Выбор инструментов (gitleaks/trufflehog; pip-audit/trivy).
|
||||
|
||||
---
|
||||
|
||||
## Решение
|
||||
|
||||
### Р-1. Размещение — Вариант M (под-гейт ребра `deploy-staging → deploy`), ПЕРВЫМ среди edge-под-гейтов
|
||||
|
||||
Security-гейт реализуется как **детерминированный под-гейт того же ребра**
|
||||
`deploy-staging → deploy`, что merge-gate и image-freshness, и исполняется **ПЕРВЫМ** —
|
||||
**ДО** merge-gate. `STAGE_TRANSITIONS` **не меняется** (триггер — то же событие «staging-
|
||||
deployer завершился»; инвариант TRZ §7.1).
|
||||
|
||||
Порядок врезок в `advance_stage` (блок `current_stage == "deploy-staging"`):
|
||||
|
||||
```
|
||||
check_staging_status (PASS, существующий QG стадии)
|
||||
→ security-gate (НОВЫЙ, _handle_security_gate) ← первым
|
||||
→ merge-gate (_handle_merge_gate)
|
||||
→ image-freshness (_handle_image_freshness)
|
||||
→ Phase A (self-deploy approve)
|
||||
```
|
||||
|
||||
**Почему merge-edge, а не review (Вариант R):**
|
||||
- BRD-требование «перед слиянием в `main`» удовлетворяют оба, но на review-стадии diff
|
||||
может разойтись с тем, что реально вольётся в `main` (параллельная задача двигает `main`
|
||||
вперёд между review и merge). Merge-edge — последняя точка перед фактическим мержем.
|
||||
- Переиспользуется готовая машинерия отката/retry/нотификаций edge-под-гейтов
|
||||
(минимальный blast-radius, инвариант TRZ §7).
|
||||
|
||||
**Почему ПЕРВЫМ (до merge-gate), а не после image-freshness:**
|
||||
- **Дёшево фейлить.** merge-gate (rebase + re-test, минуты) и image-freshness (docker
|
||||
rebuild, до 1200с) — дорогие. Нет смысла гонять их на ветке с секретом/CVE.
|
||||
- **Корректность для секретов.** Секрет живёт в собственных коммитах ветки;
|
||||
rebase онто `main` его не добавляет и не убирает → скан диапазона `origin/main..HEAD`
|
||||
до rebase ловит ровно те коммиты, что попадут в `main`.
|
||||
- **Анти-петля для зависимостей.** Аудит ветки **до** rebase оценивает то, что вносит
|
||||
ИМЕННО эта задача (её `requirements.txt`/diff), а не уязвимость, которую притащил в
|
||||
ветку обновившийся `main`. Аудит после rebase «обвинял» бы задачу в чужой (main'овой)
|
||||
CVE → ложный откат `→ development` → петля (прецедент ORCH-061). Скан до rebase этого
|
||||
избегает.
|
||||
- **Проще, чем image-freshness.** Гейт исполняется ДО захвата merge-lease → при FAIL
|
||||
**lease освобождать не нужно** (в отличие от `_handle_image_freshness`). Чистый откат.
|
||||
|
||||
**Почему не CI-job (Вариант C):** пороги severity, warning-vs-block, аллоулист и
|
||||
машиночитаемый артефакт-вердикт плохо выражаются одним статусом коммита Gitea; путь
|
||||
коуплится с CI-раннером. Отклонено для v1; оставлено как точка расширения (BR-14).
|
||||
|
||||
### Р-2. Инструменты
|
||||
|
||||
- **Secret-scanning — `gitleaks`.** Полностью **offline** (без сетевого фида → гарантия
|
||||
«секрет всегда блокирует» не зависит от сети, BR-2), один статический бинарь,
|
||||
детерминированный, конфиг + аллоулист в репо (`.gitleaks.toml`, BR-13), поддержка
|
||||
`--log-opts="origin/main..HEAD"` (скан диапазона), JSON-отчёт, exit-code контракт
|
||||
(0 = чисто, 1 = найдены секреты, ≥2 = ошибка инструмента). Бинарь устанавливается в
|
||||
`Dockerfile` (Go-бинарь, не pip-пакет) — см. `07-infra-requirements.md`.
|
||||
- **Dependency audit — `pip-audit`.** Python-native (v1-стек — сам оркестратор, Python),
|
||||
читает `requirements.txt`, источник advisory — OSV/PyPI, JSON-выход, ставится через
|
||||
`requirements.txt`. trivy/trufflehog отклонены как тяжелее/контейнер-ориентированные для
|
||||
v1-цели «Python-only» (A3).
|
||||
|
||||
Конкретные инструменты — деталь реализации; контракт гейта (вход: repo/branch/wi,
|
||||
выход: `(bool, reason)` + артефакт) от них не зависит, заменяемы за leaf-модулем.
|
||||
|
||||
### Р-3. Degrade при недоступном CVE-фиде — **fail-open + громкий warning** (дефолт)
|
||||
|
||||
`pip-audit` требует сети (OSV/PyPI advisory DB). Недоступность фида **по умолчанию**:
|
||||
- **fail-open**: dep-audit не даёт FAIL по причине недоступности фида (иначе — ложные
|
||||
откаты `→ development` → петля при сетевых проблемах прод-инстанса, прецедент ORCH-061);
|
||||
- **громко**: в артефакте `deps_audit_degraded: true`, лог `logger.warning`, Telegram-алерт.
|
||||
- **Секреты не деградируют:** gitleaks offline → гарантия BR-2 безусловна даже при
|
||||
отсутствии сети. Деградирует ТОЛЬКО dep-audit.
|
||||
- **Конфигурируемо:** флаг `security_dep_audit_fail_closed` (дефолт `false`) позволяет
|
||||
Owner'у переключить на fail-closed (недоступность фида → FAIL) без редеплоя кода.
|
||||
|
||||
Это разделяет две гарантии: «нет секрета в прод» — **безусловная**; «нет известной CVE» —
|
||||
**best-effort при доступности фида**. Закреплено в acceptance (AC-7).
|
||||
|
||||
### Р-4. Пороги классификации (A4, BR-10)
|
||||
|
||||
- **Секреты:** любой подтверждённый (не из аллоулиста) секрет → **вклад в FAIL** (всегда
|
||||
блок; флаг `security_secrets_block`, дефолт `true`).
|
||||
- **Зависимости:** severity ≥ `security_dep_block_severity` (дефолт `HIGH`) → **вклад в
|
||||
FAIL** (`deps_blocking`); ниже порога (`MEDIUM`/`LOW`) → **warning** (`deps_warning`,
|
||||
не блокирует, фиксируется в теле).
|
||||
- **Severity = UNKNOWN** (OSV/advisory без CVSS — частый случай pip-audit): трактуется как
|
||||
**ниже порога → warning**, никогда не авто-блок (анти-петля). Логируется.
|
||||
|
||||
### Р-5. Артефакт и вердикт (FR-3, BR-6, канон проекта)
|
||||
|
||||
- Новый артефакт **`17-security-report.md`** (следующий свободный номер; финализировано).
|
||||
- YAML-frontmatter:
|
||||
```
|
||||
---
|
||||
security_status: PASS # PASS | FAIL
|
||||
secrets_found: 0
|
||||
deps_blocking: 0
|
||||
deps_warning: 2
|
||||
deps_audit_degraded: false
|
||||
---
|
||||
```
|
||||
Тело — человекочитаемый список находок (секреты: файл/правило/маскированное совпадение;
|
||||
CVE: пакет/версия/идентификатор/severity).
|
||||
- **Единый источник истины:** гейт вычисляет находки → пишет артефакт → **читает вердикт
|
||||
обратно через `parse_security_status(content)`** (frontmatter-парсер по образцу
|
||||
`_parse_deploy_status`/`_parse_staging_status`) → возвращает этот вердикт. Так возвращаемый
|
||||
`(bool, reason)` гарантированно == frontmatter артефакта (канон «машинный вердикт — только
|
||||
из YAML-frontmatter, никогда из прозы», AC-8). Negative-токен (`FAIL`) авторитетен.
|
||||
- Битый/отсутствующий frontmatter / нет поля `security_status` → `(False, reason)` —
|
||||
fail-closed на чтении вердикта (AC-9).
|
||||
|
||||
### Р-6. Поведение красного гейта (FR-4, BR-5)
|
||||
|
||||
`security_status: FAIL` → врезка `_handle_security_gate` (по образцу
|
||||
`_handle_image_freshness`, но БЕЗ работы с lease — гейт до его захвата):
|
||||
- `update_task_stage(development)` + `enqueue_job("developer", …)`;
|
||||
- retry-счётчик — **существующий** `_developer_retry_count` (общий с merge/freshness;
|
||||
без новой колонки, TRZ §6); cap `MAX_DEVELOPER_RETRIES = 3` → при исчерпании
|
||||
`set_issue_blocked` + Telegram;
|
||||
- `task_desc` несёт **дословную причину** (какие секреты/файлы, какие пакеты/CVE/severity)
|
||||
по образцу ORCH-046 — не только ссылку на артефакт (AC-12);
|
||||
- `notify_qg_failure` + Plane-коммент (наблюдаемость BR-11).
|
||||
|
||||
PASS → `return False` из обработчика → `advance_stage` идёт к merge-gate (тишина, без шума).
|
||||
|
||||
### Р-7. Условный раскат и устойчивость (FR-5/FR-6)
|
||||
|
||||
- `check_security_gate(repo, work_item_id, branch)` в `QG_CHECKS`; обёртка делегирует в
|
||||
`src/security_gate.py` (ленивый импорт во избежание цикла — по образцу
|
||||
`_check_staging_image_fresh`).
|
||||
- Условность: `security_gate_enabled=False` → `(True, "security-gate disabled")`;
|
||||
`security_gate_repos` (CSV) пусто → реально только `is_self_hosting_repo` → прочие репо
|
||||
`(True, "security-gate N/A for <repo>")` (AC-14/AC-15).
|
||||
- **never-raise** (двойной guard как `check_branch_mergeable`): любая ошибка (нет бинаря,
|
||||
таймаут, исключение) → `(False, reason)`, исключение не уходит в `advance_stage` (AC-16).
|
||||
- Таймаут сканирования `security_scan_timeout_s` (дефолт 300) на каждый внешний вызов
|
||||
(`subprocess … timeout=`) — превышение → детерминированный degrade-вердикт (AC-17).
|
||||
|
||||
### Р-8. Self-hosting safety (инвариант TRZ §7.5, AC-19)
|
||||
|
||||
Гейт **только читает/сканирует** (git, gitleaks, pip-audit, запись артефакта). Не вызывает
|
||||
деплой-хук, не рестартит и не трогает прод-контейнер (8500/8501).
|
||||
|
||||
---
|
||||
|
||||
## Точки касания (для developer; reviewer проверяет полноту — AC-20)
|
||||
|
||||
| Модуль | Изменение |
|
||||
|--------|-----------|
|
||||
| `src/security_gate.py` (**новый leaf**) | `security_gate_applies`, `scan_secrets`, `audit_dependencies`, `classify_severity`, `compute_verdict`, `write_security_report`, `parse_security_status`, `check_security_gate`. never-raise, fail-closed на чтении вердикта. По образцу `image_freshness.py`. |
|
||||
| `src/qg/checks.py` | `check_security_gate` (тонкая обёртка, ленивый импорт) + регистрация в `QG_CHECKS`. |
|
||||
| `src/stage_engine.py` | `_handle_security_gate(...)` + врезка ПЕРВОЙ в блоке `current_stage == "deploy-staging"` (до `_handle_merge_gate`). FAIL → откат на `development`. never-raise. **`STAGE_TRANSITIONS` НЕ меняется.** |
|
||||
| `src/config.py` | `security_gate_enabled` (True), `security_gate_repos` (""), `security_dep_block_severity` ("HIGH"), `security_scan_timeout_s` (300), `security_dep_audit_fail_closed` (False), `security_secrets_block` (True) — с docstring по образцу ORCH-043/058. |
|
||||
| `Dockerfile` | Установка `gitleaks` (release-бинарь). |
|
||||
| `requirements.txt` | `pip-audit`. |
|
||||
| `.gitleaks.toml` (**новый, корень репо**) | Конфиг правил + аллоулист (`.env.example`-плейсхолдеры, тест-фикстуры) — BR-13. |
|
||||
| `.openclaw/agents/developer.md` | (Опц.) краткая инструкция про устранение security-находок при заворотах. |
|
||||
| `tests/` | `test_security_gate.py`, `test_qg_security.py`, `test_stage_engine_security_gate.py` (см. `04-test-plan.yaml`). |
|
||||
| **Документация** | `CLAUDE.md` (артефакт 17-…), `docs/architecture/README.md` (таблица гейтов + реестр QG + раздел), `CHANGELOG.md`, `.env.example` (`ORCH_SECURITY_*`), global `adr-0012`. |
|
||||
|
||||
---
|
||||
|
||||
## Альтернативы (отклонены)
|
||||
|
||||
- **Вариант R (review-стадия):** раньше/дешевле, но diff может разойтись с тем, что
|
||||
вольётся в `main`; merge-edge уже закрывает «последнюю страховку».
|
||||
- **Вариант C (CI-job через `check_ci_green`):** пороги/severity/аллоулист/артефакт плохо
|
||||
выражаются статусом коммита; коуплинг с CI-раннером. → точка расширения BR-14.
|
||||
- **fail-closed dep-audit по умолчанию:** ложные откаты при сетевых сбоях → петля. →
|
||||
только опционально через флаг.
|
||||
- **Аудит после rebase (как анкер image-freshness):** обвиняет задачу в CVE из `main` →
|
||||
петля. → скан ветки ДО merge-gate.
|
||||
- **Новая стадия `security`:** «пустая» стадия без агента не имеет триггера (как
|
||||
отклонено в ORCH-043). → под-гейт ребра.
|
||||
- **Новая колонка retry в БД:** не нужна — переиспользуем `_developer_retry_count`.
|
||||
|
||||
---
|
||||
|
||||
## Последствия
|
||||
|
||||
**Плюсы.** Структурно невозможно тихо влить секрет (безусловно) или известную CVE
|
||||
(best-effort) в `main`/прод автономной системы. Самоприменение CLAUDE.md §8. Минимальный
|
||||
blast-radius: `STAGE_TRANSITIONS`/схема БД не меняются, переиспользован готовый паттерн.
|
||||
|
||||
**Минусы / плата.** Ещё один «скрытый» под-гейт ребра (нет в `STAGE_TRANSITIONS`).
|
||||
Добавлены внешние инструменты (gitleaks-бинарь в образ, pip-audit в зависимости). Время
|
||||
сканирования добавляется к каждому прогону (ограничено таймаутом). Dep-audit best-effort
|
||||
при сетевых сбоях (осознанный компромисс против петли). v1 — Python-only (A3); мульти-стек
|
||||
и SAST — follow-up WI (BR-14).
|
||||
|
||||
**Раскат.** Сквозное изменение конвейера (новый QG + новый edge-под-гейт) → лейбл
|
||||
`arch:major-change`. Прод-деплой ORCH-022 — строго через staging-гейт (8501), без рестарта
|
||||
прод-контейнера в рамках задачи (self-hosting safety).
|
||||
|
||||
## Связи
|
||||
|
||||
adr-0006 (merge-gate — паттерн edge-под-гейта/отката), adr-0008 (image-freshness —
|
||||
условность/never-raise/fail-closed), adr-0003 (`is_self_hosting_repo` — образец условности),
|
||||
adr-0009/ORCH-061 (анти-петля ложных FAIL), ORCH-046 (дословный reason в `task_desc`),
|
||||
ORCH-9/15 (мульти-стек — будущая зависимость).
|
||||
56
docs/work-items/ORCH-022/07-infra-requirements.md
Normal file
56
docs/work-items/ORCH-022/07-infra-requirements.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 07 — Инфраструктурные требования: Security-гейт (ORCH-022)
|
||||
|
||||
См. `06-adr/ADR-001-security-gate.md` (Р-2, Р-3, Р-8). Топология не меняется (один сервер
|
||||
mva154, Docker Compose). Новые требования — только инструменты сканирования и сетевой доступ
|
||||
к CVE-фиду.
|
||||
|
||||
## I-1. Бинарь `gitleaks` в образе
|
||||
- **Что:** статический Go-бинарь `gitleaks` (secret-scanning), устанавливается в `Dockerfile`
|
||||
(НЕ pip-пакет). Зафиксировать версию (pinned release) для детерминизма.
|
||||
- **Почему в образе, а не на хосте:** гейт исполняется внутри контейнера оркестратора
|
||||
(`advance_stage`); сканируется per-task worktree, смонтированный в контейнер.
|
||||
- **Оффлайн:** gitleaks не требует сети (правила локальны) → гарантия «секрет всегда
|
||||
блокирует» (BR-2) не зависит от доступности интернета.
|
||||
- **Контракт exit-кодов:** 0 = чисто, 1 = найдены секреты, ≥2 = ошибка инструмента
|
||||
(≥2 → never-raise degrade-вердикт гейта).
|
||||
|
||||
## I-2. `pip-audit` в зависимостях
|
||||
- **Что:** Python-пакет `pip-audit` (dependency audit), добавляется в `requirements.txt`
|
||||
(pinned-версия).
|
||||
- **Источник advisory:** OSV / PyPI advisory DB — **требует сетевого доступа** (исходящий
|
||||
HTTPS к OSV/PyPI).
|
||||
- **Цель v1:** аудит `requirements.txt` корня репо (Python-стек, A3). Мульти-стек — follow-up.
|
||||
|
||||
## I-3. Сетевой доступ к CVE-фиду (degrade-политика)
|
||||
- **Требование:** исходящий HTTPS из прод-контейнера к OSV/PyPI advisory.
|
||||
- **При недоступности (Р-3):** **fail-open + громкий warning** по умолчанию — dep-audit не
|
||||
краснит гейт из-за сетевого сбоя (анти-петля ORCH-061); фиксируется
|
||||
`deps_audit_degraded: true` + Telegram + лог. Флаг `security_dep_audit_fail_closed`
|
||||
(дефолт `false`) — для перевода в строгий режим без редеплоя кода.
|
||||
- **Секреты не зависят от сети** (I-1) — критическая гарантия безусловна.
|
||||
|
||||
## I-4. Конфиг-файлы в репозитории (версионируемые, BR-13)
|
||||
- `.gitleaks.toml` (корень репо): правила + аллоулист заведомо-безопасных совпадений
|
||||
(плейсхолдеры `.env.example`, тест-фикстуры). Версионируется, ревьюится как код.
|
||||
|
||||
## I-5. Env-флаги (`.env.example` + хост `.env`/`.env.staging`)
|
||||
| Переменная | Дефолт | Назначение |
|
||||
|------------|--------|-----------|
|
||||
| `ORCH_SECURITY_GATE_ENABLED` | `true` | глобальный kill-switch |
|
||||
| `ORCH_SECURITY_GATE_REPOS` | `` (пусто) | CSV scope; пусто → только self-hosting |
|
||||
| `ORCH_SECURITY_DEP_BLOCK_SEVERITY` | `HIGH` | порог блокировки зависимостей |
|
||||
| `ORCH_SECURITY_SCAN_TIMEOUT_S` | `300` | таймаут каждого внешнего вызова сканера |
|
||||
| `ORCH_SECURITY_DEP_AUDIT_FAIL_CLOSED` | `false` | строгий режим при недоступном фиде |
|
||||
| `ORCH_SECURITY_SECRETS_BLOCK` | `true` | секреты блокируют (всегда по дефолту) |
|
||||
|
||||
Секреты-значения в гит НЕ коммитятся (CLAUDE.md §8) — только дефолты в `.env.example`.
|
||||
|
||||
## I-6. Ресурсы и тайминги
|
||||
- Время сканирования добавляется к каждому прогону задачи на ребре `deploy-staging → deploy`,
|
||||
ограничено `ORCH_SECURITY_SCAN_TIMEOUT_S` (по образцу `merge_retest_timeout_s`).
|
||||
- Гейт исполняется ДО merge-gate/image-freshness (дёшево фейлить до дорогих rebase/rebuild).
|
||||
|
||||
## I-7. Self-hosting safety (инвариант)
|
||||
Гейт **только читает/сканирует** (git, gitleaks, pip-audit, запись артефакта). Не вызывает
|
||||
деплой-хук, не рестартит/не трогает прод-контейнер (8500/8501). Прод-деплой ORCH-022 — строго
|
||||
через staging-гейт (8501).
|
||||
26
docs/work-items/ORCH-022/08-data-requirements.md
Normal file
26
docs/work-items/ORCH-022/08-data-requirements.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 08 — Требования к схеме БД: Security-гейт (ORCH-022)
|
||||
|
||||
## Решение: схема БД НЕ меняется
|
||||
|
||||
Миграций нет. Обоснование (соответствует TRZ §6 и паттерну edge-под-гейтов ORCH-043/058):
|
||||
|
||||
1. **Вердикт гейта — артефакт-файл** `17-security-report.md` (YAML-frontmatter), как
|
||||
`14-deploy-log.md` / `15-staging-log.md`. Не хранится в БД.
|
||||
2. **Состояние/идемпотентность** — детерминированная пересборка вердикта при каждом тике
|
||||
(гейт чистый, без долгоживущего состояния между прогонами); sentinel-файлы НЕ требуются
|
||||
(в отличие от deploy-state/post-deploy-state — там асинхронный self-restart).
|
||||
3. **Retry-счётчик** — переиспользуется существующий `_developer_retry_count(task_id)`
|
||||
(подсчёт по `jobs`/`agent_runs`), общий с merge-gate/image-freshness. **Новой колонки
|
||||
`security_retry` НЕ вводим** (TRZ §6: предпочесть подсчёт по `jobs`/`agent_runs`). Это
|
||||
корректно: security-FAIL, как merge/freshness-FAIL, откатывает на `development` и
|
||||
запускает developer — он и есть единица retry; общий cap=3 защищает от петли.
|
||||
|
||||
## Используемые существующие таблицы (без изменений)
|
||||
- `tasks` — стадия задачи (`update_task_stage` при откате на `development`).
|
||||
- `jobs` — enqueue `developer` при FAIL; основа `_developer_retry_count`.
|
||||
- `agent_runs` — usage/duration; основа подсчёта retry.
|
||||
|
||||
## Что НЕ делаем
|
||||
- Не добавляем таблицу findings/CVE-журнала (история находок — в артефактах per-task; петля
|
||||
уроков ORCH-8 читает артефакт).
|
||||
- Не добавляем колонок в `tasks`/`jobs`.
|
||||
16
docs/work-items/ORCH-022/10-tech-risks.md
Normal file
16
docs/work-items/ORCH-022/10-tech-risks.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# 10 — Технические риски: Security-гейт (ORCH-022)
|
||||
|
||||
| ID | Риск | Вероятность / Влияние | Митигация (заложена в ADR-001) |
|
||||
|----|------|----------------------|-------------------------------|
|
||||
| R-1 | **Ложные срабатывания → петля отката** `→ development` (прецедент ORCH-061 staging-loop). | Средн. / Выс. | Аллоулист `.gitleaks.toml` (BR-13); cap `MAX_DEVELOPER_RETRIES=3` → эскалация (`set_issue_blocked`+Telegram); конфигурируемый порог severity; kill-switch; UNKNOWN-severity → warning, не блок. |
|
||||
| R-2 | **Недоступность CVE-фида** даёт ложный красный/исключение. | Средн. / Выс. | fail-open + громкий warning по умолчанию (Р-3); `deps_audit_degraded:true`; флаг `security_dep_audit_fail_closed` для строгого режима. Секреты offline → не затронуты. |
|
||||
| R-3 | **Скан вешает worker-слот** (зависший gitleaks/pip-audit) → стоит конвейер всех проектов (общий инстанс, `max_concurrency`). | Низк. / Выс. | `security_scan_timeout_s` (300) на каждый внешний вызов; never-raise degrade-вердикт; гейт ПЕРВЫМ на ребре (фейлит до дорогих rebase/rebuild). |
|
||||
| R-4 | **Исключение гейта роняет `advance_stage`** → встаёт движок. | Низк. / Выс. | Двойной never-raise guard (внешний+внутренний) как `check_branch_mergeable`; AC-16/TC-11. |
|
||||
| R-5 | **Скан после rebase обвиняет задачу в CVE из `main`** → петля. | — (устранён дизайном) | Гейт исполняется ДО merge-gate (скан ветки до rebase); Р-1. |
|
||||
| R-6 | **Отсутствие бинаря `gitleaks` в образе** (забыт в Dockerfile) → гейт всегда degrade. | Низк. / Средн. | Установка в Dockerfile (I-1), pinned-версия; TC-11 (нет бинаря → `(False,reason)`, never-raise); проверяется на staging (8501) до прода. |
|
||||
| R-7 | **pip-audit без severity (UNKNOWN)** → либо ложный блок, либо пропуск. | Средн. / Средн. | UNKNOWN → warning (не блок), логируется; осознанный анти-петля компромисс; ужесточение — follow-up. |
|
||||
| R-8 | **Self-hosting: гейт трогает прод** (рестарт/деплой). | — (запрещено дизайном) | Гейт только читает/сканирует; AC-19/TC-21; прод-деплой ORCH-022 — через staging-гейт. |
|
||||
| R-9 | **Drift вердикта vs артефакта** (возврат ≠ frontmatter). | Низк. / Средн. | Единый источник: гейт пишет артефакт → читает обратно через `parse_security_status` → возвращает (Р-5); AC-8. |
|
||||
| R-10 | **Регресс существующих гейтов/стадий** (сломан `QG_CHECKS`/`STAGE_TRANSITIONS`). | Низк. / Выс. | `STAGE_TRANSITIONS` не меняется; новый чек — аддитивно в реестр; полный прогон `tests/` (TC-20); staging-гейт перед прод. |
|
||||
| R-11 | **v1 Python-only** — секреты/CVE в не-Python стеке (JS/Android) не ловятся. | — (вне scope v1, A3) | Условность scope; точка расширения мульти-стек/SAST (BR-14); зависимость ORCH-9/15 зафиксирована. |
|
||||
| R-12 | **Стоимость времени** на каждом прогоне задачи. | Низк. / Низк. | Таймаут; гейт первым (ранний выход); только self-hosting по умолчанию. |
|
||||
74
docs/work-items/ORCH-022/12-review.md
Normal file
74
docs/work-items/ORCH-022/12-review.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-022
|
||||
verdict: APPROVED
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-022
|
||||
|
||||
## Summary
|
||||
Security-гейт (secret-scanning `gitleaks` + dependency audit `pip-audit`) реализован как
|
||||
детерминированный под-гейт ребра `deploy-staging → deploy`, исполняемый ПЕРВЫМ среди
|
||||
edge-под-гейтов — в точности по ADR-001 (Вариант M) и эталонному паттерну соседей
|
||||
(merge-gate ORCH-043 / image-freshness ORCH-058): leaf-модуль `src/security_gate.py`
|
||||
(never-raise) + тонкая обёртка `check_security_gate` в `QG_CHECKS` (lazy-import, нет цикла)
|
||||
+ врезка `_handle_security_gate` ПЕРВОЙ в блоке `current_stage == "deploy-staging"`.
|
||||
`STAGE_TRANSITIONS` и схема БД не тронуты. Все 772 теста зелёные (25 из них —
|
||||
security-специфичные: `test_security_gate.py`, `test_qg_security.py`,
|
||||
`test_stage_engine_security_gate.py`). Документация обновлена полностью и в этом же PR.
|
||||
|
||||
### Соответствие ТЗ (02-trz)
|
||||
- FR-1 secret-scan offline `origin/main..HEAD`, любой секрет вне аллоулиста → FAIL ✓
|
||||
- FR-2 dep-audit по severity (`HIGH` дефолт), MEDIUM/LOW/UNKNOWN → warning ✓
|
||||
- FR-3 машинный вердикт ТОЛЬКО из frontmatter `17-security-report.md`, negative-токен
|
||||
авторитетен, write→read-back (единый источник истины) ✓
|
||||
- FR-4 FAIL → откат на `development` + developer-retry (cap 3) + `task_desc` с дословными
|
||||
находками (ORCH-046) ✓
|
||||
- FR-5 условность `security_gate_enabled` / `security_gate_repos` (пусто → self-hosting) ✓
|
||||
- FR-6 never-raise + таймаут `security_scan_timeout_s` ✓
|
||||
- FR-7 наблюдаемость (Telegram при degraded/FAIL, лог при PASS) ✓
|
||||
- §6 без миграций БД, §7 инварианты соблюдены (STAGE_TRANSITIONS/QG_CHECKS консистентны,
|
||||
gate не деплоит/не рестартит прод) ✓
|
||||
|
||||
### Соответствие ADR (06-adr/ADR-001 + global adr-0012)
|
||||
Р-1 (размещение ПЕРВЫМ, до merge-gate, до захвата merge-lease → lease не освобождается),
|
||||
Р-2 (gitleaks pinned Go-бинарь в Dockerfile, pip-audit в requirements), Р-3 (fail-open
|
||||
degrade + флаг `security_dep_audit_fail_closed`), Р-4 (пороги, UNKNOWN→warning), Р-5
|
||||
(артефакт + read-back), Р-6 (откат/cap/эскалация), Р-7 (lazy-import, double-guard
|
||||
never-raise), Р-8 (self-hosting safety) — все реализованы как описано.
|
||||
|
||||
### Критерии приёмки (03)
|
||||
AC-1..AC-21 покрыты тестами TC-01..TC-21 (incl. rollback TC-16, verbatim task_desc TC-17,
|
||||
cap+blocked TC-18, PASS-advance TC-19, no-deploy-on-FAIL TC-21). AC-20 (документация) —
|
||||
подтверждён ниже.
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- нет
|
||||
|
||||
### P1 — Must fix
|
||||
- нет
|
||||
|
||||
### P2 — Should fix
|
||||
- нет
|
||||
|
||||
### P3 — Nice-to-have
|
||||
- Глобальный `docs/architecture/adr/adr-0012-security-gate.md` помечен `Статус: proposed`,
|
||||
тогда как per-WI `06-adr/ADR-001` — `Accepted`. Косметическая рассинхронизация статуса,
|
||||
на функциональность/гейты не влияет.
|
||||
|
||||
## Документация
|
||||
Обновлена в том же PR (AC-20, CLAUDE.md §6 соблюдён):
|
||||
- `CLAUDE.md` — раздел «Артефакты задачи» (добавлен `17-security-report.md`) + строка о
|
||||
машинных вердиктах (`security_status:`).
|
||||
- `docs/architecture/README.md` — реестр `QG_CHECKS` (`check_security_gate (ORCH-022)`),
|
||||
новый раздел «Security-гейт …», статусная сноска внизу.
|
||||
- `docs/architecture/adr/adr-0012-security-gate.md` — новый global ADR (+ per-WI ADR-001).
|
||||
- `CHANGELOG.md` — подробная запись в `[Unreleased] / Added`.
|
||||
- `.env.example` — все шесть `ORCH_SECURITY_*` с комментариями.
|
||||
- `Dockerfile` (pinned gitleaks), `requirements.txt` (pip-audit), `.gitleaks.toml` (корень,
|
||||
правила + аллоулист) — инфраструктура версионирована.
|
||||
|
||||
Статус: документация = golden source — синхронна с кодом. Замечаний нет.
|
||||
76
docs/work-items/ORCH-022/13-test-report.md
Normal file
76
docs/work-items/ORCH-022/13-test-report.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-022
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-022
|
||||
|
||||
Security-гейт: secret-scanning (gitleaks) + dependency audit (pip-audit) как под-гейт
|
||||
ребра `deploy-staging → deploy`.
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3
|
||||
- Дата: 2026-06-07
|
||||
- Ветка: `feature/ORCH-022-security-secret-scanning`
|
||||
- Review verdict: APPROVED (`12-review.md`)
|
||||
|
||||
## Smoke test API (prod 8500, self-hosting — не трогаем контейнер)
|
||||
| Endpoint | Результат |
|
||||
|----------|-----------|
|
||||
| `GET /health` | `{"status":"ok","service":"orchestrator"}` — OK |
|
||||
| `GET /status` | OK (active task ORCH-022 в stage=testing виден) |
|
||||
| `GET /queue` | OK (counts/resilience/reconcile/reaper/post_deploy присутствуют) |
|
||||
|
||||
## Результаты (привязка к 04-test-plan.yaml)
|
||||
|
||||
| TC ID | Описание | Тест | Результат |
|
||||
|-------|----------|------|-----------|
|
||||
| TC-01 | Секрет в diff → FAIL, secrets_found>=1, причина называет находку | test_security_gate.py::test_tc01_secret_in_diff_fails | PASS |
|
||||
| TC-02 | Чистая ветка → PASS, secrets_found=0 | test_tc02_clean_branch_passes | PASS |
|
||||
| TC-03 | Аллоулист подавляет заведомо-безопасное | test_tc03_allowlisted_match_does_not_fail | PASS |
|
||||
| TC-04 | HIGH/CRITICAL CVE при пороге HIGH → FAIL, deps_blocking>=1 | test_tc04_high_cve_at_high_threshold_blocks | PASS |
|
||||
| TC-05 | Только MEDIUM/LOW → PASS, deps_warning>=1 | test_tc05_only_medium_low_warns_passes | PASS |
|
||||
| TC-06 | Конфиг порога severity влияет на классификацию | test_tc06_threshold_config_changes_classification | PASS |
|
||||
| TC-07 | Недоступный фид → детерминированный degrade (fail-open default / fail-closed strict) | test_tc07_degraded_feed_failopen_default_failclosed_strict | PASS |
|
||||
| TC-08 | Вердикт ТОЛЬКО из frontmatter; negative-токен авторитетен | test_tc08_verdict_only_from_frontmatter | PASS |
|
||||
| TC-09 | Нет/битый frontmatter → (False, reason) fail-closed | test_tc09_missing_or_broken_frontmatter_failclosed | PASS |
|
||||
| TC-10 | Артефакт 17-security-report.md с валидным frontmatter + телом | test_tc10_artifact_has_valid_frontmatter_and_body | PASS |
|
||||
| TC-11 | Нет бинаря / исключение → (False, reason), never-raise | test_tc11_missing_binary_failclosed_never_raises | PASS |
|
||||
| TC-12 | Таймаут → детерминированный fail-closed, без зависания | test_tc12_timeout_is_deterministic_failclosed | PASS |
|
||||
| TC-13 | Не-self репо при пустом scope → (True, N/A) мгновенно | test_qg_security.py::test_tc13_non_self_repo_empty_scope_is_na | PASS |
|
||||
| TC-14 | ORCH_SECURITY_GATE_ENABLED=false → no-op pass | test_tc14_disabled_is_noop_pass | PASS |
|
||||
| TC-15 | Зарегистрирован в QG_CHECKS и диспетчеризуется _run_qg | test_tc15_registered_in_qg_checks / test_tc15_dispatched_by_run_qg | PASS |
|
||||
| TC-16 | FAIL → откат на development, enqueue developer, notify_qg_failure | test_stage_engine_security_gate.py::test_tc16_fail_rolls_back_and_enqueues_developer | PASS |
|
||||
| TC-17 | task_desc несёт дословную причину (ORCH-046) | test_tc17_task_desc_has_verbatim_findings | PASS |
|
||||
| TC-18 | После MAX_DEVELOPER_RETRIES (3) → set_issue_blocked + Telegram | test_tc18_retry_cap_blocks_and_alerts | PASS |
|
||||
| TC-19 | PASS → штатное продвижение конвейера | test_tc19_pass_advances_normally | PASS |
|
||||
| TC-20 | STAGE_TRANSITIONS не изменён; тесты стадий зелёные | tests/test_stages.py (полный прогон) | PASS |
|
||||
| TC-21 | Гейт не вызывает деплой-хук/рестарт прод (self-hosting safety) | test_tc21_fail_never_triggers_deploy | PASS |
|
||||
|
||||
Все 21 TC покрыты и зелёные. Соответствие критериям приёмки (03-acceptance-criteria):
|
||||
AC-1..AC-21 закрыты соответствующими TC (AC-N ↔ TC-N для N=1..21; AC-20 «документация»
|
||||
подтверждён в review 12-review.md).
|
||||
|
||||
## Вывод pytest
|
||||
|
||||
### Security-специфичные тесты (25 шт.)
|
||||
```
|
||||
tests/test_security_gate.py ............... (15)
|
||||
tests/test_qg_security.py ...... (6)
|
||||
tests/test_stage_engine_security_gate.py ..... (5)
|
||||
======================== 25 passed, 1 warning in 0.49s =========================
|
||||
```
|
||||
|
||||
### Полный регресс
|
||||
```
|
||||
======================= 772 passed, 1 warning in 14.70s ========================
|
||||
```
|
||||
(1 warning — PydanticDeprecatedSince20 в src/config.py, не связан с ORCH-022,
|
||||
существовал до задачи.)
|
||||
|
||||
## Итог
|
||||
**PASS** — полный регресс 772/772 зелёный, 25 security-тестов покрывают все 21 TC
|
||||
плана и AC-1..AC-21, smoke-тесты API прод-инстанса OK. Прод-контейнер в процессе
|
||||
тестирования не затронут (тесты офлайн/изолированы). Задача готова к стадии deploy-staging.
|
||||
12
docs/work-items/ORCH-022/14-deploy-log.md
Normal file
12
docs/work-items/ORCH-022/14-deploy-log.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
deploy_status: SUCCESS
|
||||
work_item: ORCH-022
|
||||
hook_exit_code: 0
|
||||
deployed_by: deploy-finalizer
|
||||
---
|
||||
|
||||
# Deploy log — ORCH-036 executable self-deploy
|
||||
|
||||
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
|
||||
|
||||
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.
|
||||
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`.
|
||||
14
docs/work-items/ORCH-022/16-post-deploy-log.md
Normal file
14
docs/work-items/ORCH-022/16-post-deploy-log.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
post_deploy_status: HEALTHY
|
||||
action_taken: NONE
|
||||
work_item: ORCH-022
|
||||
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.
|
||||
7
docs/work-items/ORCH-026/00-business-request.md
Normal file
7
docs/work-items/ORCH-026/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: Управление зависимостями задач (B ждёт A) в очереди
|
||||
|
||||
Work Item ID: ORCH-026
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
135
docs/work-items/ORCH-026/01-brd.md
Normal file
135
docs/work-items/ORCH-026/01-brd.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# 01-BRD — Управление зависимостями задач (B ждёт A) в очереди
|
||||
|
||||
**Work Item:** ORCH-026
|
||||
**Repo:** orchestrator (self-hosting)
|
||||
**Branch:** feature/ORCH-026-b-a
|
||||
**Стадия:** analysis
|
||||
**Источник:** предложение Стрим, одобрено Славой (2026-06-04); дополнение Слава+Стрим 2026-06-08 (инцидент эрозии `main`)
|
||||
|
||||
---
|
||||
|
||||
## 1. Контекст и проблема
|
||||
|
||||
### 1.1 Первопричина (мотивация СЕЙЧАС — инцидент 08.06)
|
||||
Эрозия `main` 08.06 (потеря кода ORCH-067/069, фантом-merge) родилась НЕ из логических
|
||||
зависимостей, а из **некоординированного параллелизма**: несколько self-hosting задач
|
||||
(ORCH-067/069/071) одновременно срезали ветки от `main` и правили общие файлы
|
||||
(`CHANGELOG.md`, `notifications.py`, `config.py`). Последствия:
|
||||
|
||||
- CHANGELOG-конфликты на `auto_rebase` → откаты `deploy-staging → development` (дорого:
|
||||
ORCH-069 = 3 попытки = $3.98);
|
||||
- тихое затирание кода соседа при merge ветки, срезанной от устаревшего `main` (фантом).
|
||||
|
||||
**ORCH-073** закрыл ПОСЛЕДСТВИЯ (3 рубежа: CHANGELOG `merge=union` + SHA-in-main verify +
|
||||
регресс-гард маркеров). ORCH-026 должен закрыть **ПЕРВОПРИЧИНУ**: задачи одного репо не
|
||||
должны мешать друг другу в `main`.
|
||||
|
||||
### 1.2 Исходный скоуп (плоская очередь ORCH-1)
|
||||
Очередь (`src/queue_worker.py`, ORCH-1) — плоская: `jobs` упорядочены по `id` (FIFO),
|
||||
гейтятся только `available_at` и `max_concurrency`. Нельзя выразить «задача B не стартует,
|
||||
пока не готова A». Декомпозиция эпиков (ORCH-025) порождает заведомо зависимые подзадачи.
|
||||
|
||||
### 1.3 Что уже есть (опора, НЕ переписывать)
|
||||
- **ORCH-1** — персистентная очередь (`jobs`), atomic claim, `available_at`-defer, restart-safe.
|
||||
- **ORCH-065** — `merge-lease` (`src/merge_gate.py`): per-repo файловый лиз
|
||||
`.merge-lease-<repo>.json`, неблокирующий acquire, holder-aware release, проактивный
|
||||
реклейм мёртвого/устаревшего держателя. **Сейчас лиз держится только на ребре
|
||||
`deploy-staging → deploy`** (от merge-gate до фактического merge).
|
||||
- **ORCH-043** — merge-gate: `branch_is_behind_main`, `auto_rebase_onto_main` (rebase
|
||||
**только когда ветка отстаёт или при конфликте**), `retest_branch`.
|
||||
- **ORCH-073** — merge-verify: `verify_merged_to_main` (SHA-in-main), `check_main_regression`.
|
||||
- **Plane-статусы** `Blocked` / `Needs Input` + `set_issue_blocked` (`src/plane_sync.py`).
|
||||
- **Telegram live-tracker** (`src/notifications.py`) — одна карточка на задачу, уже умеет
|
||||
показывать статус `Blocked`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Цель (бизнес-результат)
|
||||
|
||||
Задачи одного репозитория перестают повреждать `main` друг друга, а очередь умеет
|
||||
выражать логические зависимости между задачами — БЕЗ потери параллелизма между разными
|
||||
репозиториями и без риска для self-hosting прода.
|
||||
|
||||
---
|
||||
|
||||
## 3. Два уровня требований (объединить в одной задаче; приоритет — Уровень A)
|
||||
|
||||
### Уровень A — Сериализация merge/деплоя внутри ОДНОГО репо (КРИТИЧНО, корень эрозии)
|
||||
Закрывает первопричину инцидента 08.06.
|
||||
|
||||
- **A-1.** В рамках ОДНОГО репо merge-в-`main` + деплой должны быть **сериализованы**: пока
|
||||
задача A не слита в `main` (и для self-hosting — не задеплоена), задача B того же репо НЕ
|
||||
доходит до своего merge/деплоя от устаревшего `main`.
|
||||
- **A-2.** B перед своим merge-gate **обязана ребейзнуться на СВЕЖИЙ `main`** (где уже есть
|
||||
A) — **proactive pre-merge rebase**, а не только при текстовом конфликте (как сейчас в
|
||||
ORCH-043). Цель: B всегда несёт актуальный код предшественников → структурный анти-фантом
|
||||
на уровне планировщика (дополняет рубежи ORCH-073, не заменяет).
|
||||
- **A-3.** Сериализация — **только внутри одного репо**. Задачи РАЗНЫХ репо (orchestrator vs
|
||||
enduro-trails) параллелятся свободно (общая БД/очередь — пропускная способность не падает).
|
||||
- **A-4.** Механизм — минимально-инвазивный и **restart-safe** (как ORCH-1/065): переживает
|
||||
рестарт прод-контейнера, не оставляет навсегда захваченных ресурсов (опора на проактивный
|
||||
реклейм ORCH-065).
|
||||
- **A-5.** **Совместимость с self-hosting safety:** не ронять/не рестартить прод-контейнер
|
||||
вне штатного deploy; гейт `Confirm Deploy` (ORCH-059) сохранён; никаких push/force-push в
|
||||
`main`.
|
||||
- **A-6.** Защита от взаимоблокировки: B при занятой сериализации **defer** (повторная
|
||||
постановка с задержкой через `available_at`), а НЕ откат на `development` и НЕ вечное
|
||||
ожидание; bounded defer-бюджет (анти-livelock, как `merge_defer_max_attempts`).
|
||||
|
||||
### Уровень B — Декларативные зависимости (исходный скоуп ORCH-26)
|
||||
- **B-1.** Задача может объявить связь `blocked-by` / `blocks` (depends-on).
|
||||
- **B-2.** Планировщик очереди (ORCH-1) **не запускает** заблокированную задачу, пока все её
|
||||
depends-on не достигли терминального состояния (`done`).
|
||||
- **B-3.** **Защита от дедлоков:** циклические зависимости детектируются; задача в цикле не
|
||||
«пропадает молча» — выставляется `Blocked` + alert (Telegram/Plane).
|
||||
- **B-4.** **Видимость:** заблокированная задача видна — Plane-статус `Blocked` и/или
|
||||
ожидание в Telegram-карточке (что и кого ждёт).
|
||||
|
||||
---
|
||||
|
||||
## 4. Открытые вопросы для архитектора (НЕ решаются на этапе анализа)
|
||||
|
||||
> Аналитик фиксирует требования; выбор механизма — за архитектором (ADR в `06-adr/`).
|
||||
|
||||
1. **Где хранить связи (Уровень B):** Plane relations (родное, видимо в UI, но требует
|
||||
сетевого запроса и зависит от Plane) vs таблица в БД (`job_deps`/поля `tasks`, надёжно и
|
||||
offline, но дубль источника) vs **гибрид** (Plane — источник декларации, БД — кэш для
|
||||
планировщика). Рекомендация анализа: гибрид с offline-fallback (см. §6).
|
||||
2. **Механизм сериализации (Уровень A):** глобальный per-repo merge-lock vs FIFO merge-queue
|
||||
vs **обязательный pre-merge rebase + расширение окна merge-lease** (от «момента merge» до
|
||||
«main-updated»). Выбрать минимально-инвазивный, restart-safe, переиспользующий ORCH-065/043.
|
||||
3. **Граница окна сериализации для self-hosting:** для не-self репо «merged в main» = конец
|
||||
окна; для self (orchestrator) деплой асинхронный (Phase B/C, ORCH-036/071) — нужно решить,
|
||||
до какого события держать лиз (до `merged_to_main: true` / до `done`).
|
||||
4. **Совместимость B и A:** depends-on (B) на уровне постановки в очередь vs merge-сериализация
|
||||
(A) на уровне merge-gate — разные точки конвейера; убедиться, что не конфликтуют.
|
||||
|
||||
---
|
||||
|
||||
## 5. Вне скоупа (Non-goals)
|
||||
- Изменение машины стадий `STAGE_TRANSITIONS` (сериализация/зависимости — врезки/гейты, не
|
||||
новые стадии — паттерн ORCH-043/058/071).
|
||||
- Приоритизация/перепланирование задач по весам (только зависимости и сериализация).
|
||||
- Кросс-репо зависимости (A-3 явно запрещает кросс-репо сериализацию; кросс-репо логические
|
||||
зависимости — возможный follow-up, не v1).
|
||||
- Отмена/замена рубежей ORCH-073 — ORCH-026 их **дополняет** на уровне планировщика.
|
||||
|
||||
---
|
||||
|
||||
## 6. Заинтересованные стороны
|
||||
- **Owner (Слава)** — одобряет BRD; держатель self-hosting прод-риска.
|
||||
- **Стрим** — автор предложения.
|
||||
- **Конвейер агентов** — потребитель: developer/deployer работают с веткой, которую затрагивает
|
||||
сериализация; reviewer проверяет обновление доки.
|
||||
|
||||
---
|
||||
|
||||
## 7. Критерии успеха (бизнес-уровень)
|
||||
- Две зелёные задачи одного репо больше не способны затереть код друг друга в `main` на уровне
|
||||
планировщика (без участия рубежей-последствий ORCH-073).
|
||||
- Задача может объявить зависимость; заблокированная задача не стартует раньше времени и видна
|
||||
наблюдателю.
|
||||
- Пропускная способность разных репо не деградирует.
|
||||
- Прод-контейнер orchestrator не падает и не рестартится вне штатного `Confirm Deploy`.
|
||||
|
||||
Точные PASS/FAIL — `03-acceptance-criteria.md`.
|
||||
134
docs/work-items/ORCH-026/02-trz.md
Normal file
134
docs/work-items/ORCH-026/02-trz.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# 02-ТЗ — Управление зависимостями задач (B ждёт A) в очереди
|
||||
|
||||
**Work Item:** ORCH-026 · **Repo:** orchestrator · **Стадия:** analysis
|
||||
|
||||
> ТЗ фиксирует ТРЕБОВАНИЯ к изменениям (модули, контракты, артефакты). Конкретный механизм
|
||||
> сериализации и место хранения связей — решение архитектора (ADR в `06-adr/`); ниже отмечены
|
||||
> как «КАНДИДАТ / решает архитектор». Аналитик не предлагает архитектуру.
|
||||
|
||||
---
|
||||
|
||||
## 1. Задействованные модули `src/`
|
||||
|
||||
| Модуль | Роль в задаче | Уровень |
|
||||
|--------|---------------|---------|
|
||||
| `src/queue_worker.py` | Планировщик: `_drain_once` / `claim_next_job` — точка учёта зависимостей и сериализации при выборе job. | A + B |
|
||||
| `src/db.py` | Очередь `jobs` / `tasks`; `claim_next_job`, `enqueue_job`, `count_running_jobs`. Кандидат на хранение связей и блокировки claim. | A + B |
|
||||
| `src/merge_gate.py` | merge-lease (ORCH-065), `branch_is_behind_main` / `auto_rebase_onto_main` (ORCH-043) — опора для proactive pre-merge rebase и расширения окна сериализации. | A |
|
||||
| `src/qg/checks.py` | `check_branch_mergeable` (под-гейт ребра `deploy-staging → deploy`) — точка форсированного pre-merge rebase. | A |
|
||||
| `src/stage_engine.py` | `advance_stage` — врезки гейтов; точка интеграции сериализации/верификации. | A |
|
||||
| `src/webhooks/plane.py` | `handle_work_item_created` / `start_pipeline` — приём задачи; точка чтения relations (если источник — Plane). | B |
|
||||
| `src/plane_sync.py` | `set_issue_blocked`, `get_project_states` (`blocked`/`needs_input`), relations API. | B |
|
||||
| `src/notifications.py` | live-карточка: индикация `Blocked` / «ждёт ORCH-NNN». | B |
|
||||
| `src/config.py` | Новые kill-switch + scope-настройки (паттерн `*_enabled` / `*_repos`). | A + B |
|
||||
| `src/reconciler.py` / `src/job_reaper.py` | Не ломать: skip заблокированных задач (как уже делается для Blocked/Needs-Input, ORCH-060/068); реклейм ресурсов сериализации. | A + B |
|
||||
|
||||
---
|
||||
|
||||
## 2. Требования к изменениям — Уровень A (сериализация merge/деплоя)
|
||||
|
||||
### 2.1 Proactive pre-merge rebase (A-2)
|
||||
- На ребре `deploy-staging → deploy`, ДО фактического merge (в составе `check_branch_mergeable`
|
||||
или соседнего под-гейта), ветка задачи **всегда** догоняется на свежий `origin/main` —
|
||||
**не только при `branch_is_behind_main`/конфликте**.
|
||||
- Переиспользовать `merge_gate.auto_rebase_onto_main` (rebase + `push --force-with-lease`
|
||||
ТОЛЬКО ветки задачи). Текстовый конфликт → существующий контракт: `rebase --abort` → откат на
|
||||
`development` (как ORCH-043).
|
||||
- **Инвариант:** никаких push/force-push в `main`.
|
||||
|
||||
### 2.2 Расширение окна merge-lease (A-1, A-3, A-4)
|
||||
- **КАНДИДАТ (решает архитектор):** держать per-repo merge-lease (ORCH-065) не только «на
|
||||
момент merge», а на окно **«merge → main-updated»** (для self — до подтверждения
|
||||
`merged_to_main: true` / `done`), чтобы B не дошла до своего merge, пока A не в `main`.
|
||||
- Acquire — **неблокирующий** (как сейчас): занято → **defer** задачи B через
|
||||
`enqueue_job(available_at_delay_s=...)`, bounded бюджет (анти-livelock; ср.
|
||||
`merge_defer_max_attempts`). Откат на `development` НЕ применять для defer.
|
||||
- Release — holder-aware (как `release_merge_lease`), на merged-вебхуке / `deploy→done` /
|
||||
откате / по проактивному реклейму (ORCH-065 `reclaim_stale_lease`).
|
||||
- Сериализация **строго per-repo** (`.merge-lease-<repo>.json`) — кросс-репо параллелизм не
|
||||
затрагивается (A-3).
|
||||
|
||||
### 2.3 Условность и безопасность (A-5)
|
||||
- Реально только для применимых репо: kill-switch + CSV-scope (паттерн `merge_gate_repos` /
|
||||
`merge_verify_repos`; пусто → только self-hosting `orchestrator`).
|
||||
- `STAGE_TRANSITIONS`, `Confirm Deploy` (ORCH-059), exit-коды deploy-хука, БАГ-8,
|
||||
terminal-sync — **без изменений**.
|
||||
- Контракт **never-raise** для всех новых функций (как соседи в `merge_gate.py`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Требования к изменениям — Уровень B (декларативные зависимости)
|
||||
|
||||
### 3.1 Декларация связи (B-1)
|
||||
- **КАНДИДАТ хранения (решает архитектор, см. BRD §4.1):**
|
||||
- вариант Plane relations: читать `blocked-by` через Plane API в `handle_work_item_created`;
|
||||
- вариант БД: новая таблица `job_deps(task_id, depends_on_task_id)` или поле в `tasks`
|
||||
(idempotent `_ensure_column` миграция, как ORCH-065 `jobs.pid`);
|
||||
- гибрид: Plane — декларация, БД — кэш для планировщика (offline-устойчивость).
|
||||
- Миграция БД (если выбран вариант с таблицей/колонкой) — **только аддитивная**
|
||||
(`CREATE TABLE IF NOT EXISTS` / `_ensure_column`), безопасная на живой прод-БД с общими
|
||||
данными enduro-trails.
|
||||
|
||||
### 3.2 Гейт планировщика (B-2)
|
||||
- При выборе job (`claim_next_job` / `_drain_once`) задача с незавершёнными depends-on
|
||||
**не клеймится** (аналог `available_at`-gate): пропускается до тех пор, пока все depends-on
|
||||
не `done`. Не должна занимать слот `max_concurrency`.
|
||||
- Реализация — **leaf-функция** с чистой логикой «готова ли задача к запуску» (тестируемо
|
||||
юнитами, never-raise), по образцу `staging_verdict.py` / `post_deploy.py`.
|
||||
|
||||
### 3.3 Защита от дедлоков (B-3)
|
||||
- Детектор циклов в графе depends-on (DFS/обнаружение цикла) — чистая функция, юнит-тестируемая.
|
||||
- Цикл → задача(и) НЕ запускается молча: `set_issue_blocked` + alert (Telegram/Plane) с
|
||||
указанием цикла. Не блокировать поток других задач.
|
||||
|
||||
### 3.4 Видимость (B-4)
|
||||
- Заблокированная задача: Plane-статус `Blocked` (`set_issue_blocked`) и/или строка ожидания в
|
||||
Telegram-карточке («⏳ ждёт ORCH-NNN»). Использовать существующий механизм карточки
|
||||
(`notifications.update_task_tracker`), контракт never-raise / silent.
|
||||
- `reconciler` F-1 уже пропускает Blocked/Needs-Input (ORCH-060/068) — убедиться, что новые
|
||||
заблокированные-по-зависимости задачи тоже пропускаются (не «разблокируются» ошибочно).
|
||||
|
||||
---
|
||||
|
||||
## 4. Изменения API (endpoints)
|
||||
- **Новые HTTP endpoints не требуются.**
|
||||
- **Наблюдаемость:** расширить снимок `GET /queue` блоком о зависимостях/сериализации
|
||||
(по образцу блоков `reconcile` / `reaper` / `post_deploy` / `merge_verify`): кол-во
|
||||
заблокированных задач, держатель merge-lease, defer-счётчики, обнаруженные циклы. Read-only,
|
||||
никогда не источник истины для решений.
|
||||
|
||||
## 5. Изменения схемы БД
|
||||
- **КАНДИДАТ (если выбран БД/гибрид для Уровня B):** аддитивная таблица `job_deps` или колонка
|
||||
в `tasks` (см. §3.1). Только `CREATE TABLE IF NOT EXISTS` / `_ensure_column`. Без изменения
|
||||
существующих колонок `jobs`/`tasks`. Restart-safe, безопасно на общей прод-БД.
|
||||
- Уровень A (сериализация) — **без изменения схемы БД** (merge-lease файловый, как ORCH-065).
|
||||
|
||||
## 6. Требования к новым QG checks
|
||||
- **Новый зарегистрированный QG-чек НЕ вводится** (паттерн ORCH-071/058: под-гейт — врезка в
|
||||
`advance_stage` или расширение `check_branch_mergeable`, а не новая запись в `QG_CHECKS`).
|
||||
- Реестр `QG_CHECKS` — без изменений.
|
||||
|
||||
## 7. Конфигурация (`src/config.py`)
|
||||
Новые настройки по паттерну `*_enabled` (kill-switch) + `*_repos` (CSV scope, пусто →
|
||||
self-hosting). КАНДИДАТ-имена (финализирует архитектор):
|
||||
- Уровень A: `merge_serialize_enabled` / `merge_serialize_repos` (или расширение
|
||||
`merge_gate_*`); опционально `premerge_rebase_always` (вкл proactive rebase).
|
||||
- Уровень B: `task_deps_enabled` / `task_deps_source` (`plane|db|hybrid`).
|
||||
Дефолты — обратная совместимость (для не-self репо — прежнее поведение).
|
||||
|
||||
## 8. Артефакты pipeline (создать/обновить В ТОМ ЖЕ PR)
|
||||
- `06-adr/ADR-001-*.md` — решение по сериализации (A) и хранению зависимостей (B).
|
||||
- Обновить `docs/architecture/README.md` (раздел про очередь/merge-gate/сериализацию).
|
||||
- Обновить `CLAUDE.md` (паспорт: конвейер/инварианты, если меняется поведение очереди).
|
||||
- Обновить `CHANGELOG.md` (`## [Unreleased]`).
|
||||
- Если вводится таблица БД — отразить в `08-data-requirements.md` (создаёт архитектор).
|
||||
- `07-infra-requirements.md` — если требуется новый Plane-статус/настройка relations.
|
||||
|
||||
## 9. Инварианты (НЕ нарушать)
|
||||
1. `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_deploy_status`/`check_staging_status`,
|
||||
`Confirm Deploy` (ORCH-059), БАГ-8, terminal-sync — без изменений.
|
||||
2. Никаких push/force-push в `main`; force только `--force-with-lease` на ветку задачи.
|
||||
3. Сериализация — строго per-repo; кросс-репо параллелизм сохранён.
|
||||
4. never-raise во всех новых функциях; restart-safe состояние.
|
||||
5. ORCH-026 дополняет рубежи ORCH-073, не заменяет.
|
||||
6. Прод-контейнер orchestrator не рестартится вне штатного `Confirm Deploy`.
|
||||
107
docs/work-items/ORCH-026/03-acceptance-criteria.md
Normal file
107
docs/work-items/ORCH-026/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# 03-Критерии приёмки — ORCH-026
|
||||
|
||||
**Work Item:** ORCH-026 · **Repo:** orchestrator · **Стадия:** analysis
|
||||
|
||||
Каждый критерий — проверяемое условие PASS/FAIL. Маппинг на тесты — `04-test-plan.yaml`.
|
||||
|
||||
---
|
||||
|
||||
## Уровень A — Сериализация merge/деплоя внутри одного репо
|
||||
|
||||
### AC-A1 — Сериализация merge внутри репо
|
||||
- **PASS:** пока задача A применимого репо удерживает окно merge (merge-lease не освобождён /
|
||||
`main` ещё не обновлён), задача B того же репо НЕ доходит до фактического merge — она
|
||||
**defer**-ится (повторная постановка через `available_at`), а не мержится от устаревшего `main`.
|
||||
- **FAIL:** B мержится/деплоится, пока A не в `main`; или B откатывается на `development` вместо
|
||||
defer.
|
||||
|
||||
### AC-A2 — Proactive pre-merge rebase
|
||||
- **PASS:** перед merge ветка задачи **всегда** догоняется на свежий `origin/main` (вызывается
|
||||
rebase), даже когда текстового конфликта нет и ветка формально не «behind» по старой проверке;
|
||||
после rebase ветка содержит код предшественника (A).
|
||||
- **FAIL:** rebase запускается только при конфликте/`branch_is_behind_main`, и B мержится без
|
||||
кода A.
|
||||
|
||||
### AC-A3 — Кросс-репо параллелизм сохранён
|
||||
- **PASS:** задача в `orchestrator` и задача в `enduro-trails` доходят до merge/деплоя
|
||||
параллельно — сериализация одного репо не блокирует другой (lease/гейт строго per-repo).
|
||||
- **FAIL:** задача одного репо ждёт освобождения ресурса, удерживаемого задачей ДРУГОГО репо.
|
||||
|
||||
### AC-A4 — Restart-safe
|
||||
- **PASS:** после рестарта прод-контейнера состояние сериализации восстанавливается корректно;
|
||||
мёртвый держатель merge-lease проактивно реклеймится (ORCH-065), конвейер не встаёт навсегда.
|
||||
- **FAIL:** рестарт оставляет навсегда захваченный lease → конвейер всех проектов встаёт.
|
||||
|
||||
### AC-A5 — Self-hosting safety
|
||||
- **PASS:** прод-контейнер orchestrator НЕ рестартится/не падает вне штатного `Confirm Deploy`
|
||||
(ORCH-059); нет push/force-push в `main`; `STAGE_TRANSITIONS` и реестр `QG_CHECKS` не изменены.
|
||||
- **FAIL:** любой незапрошенный рестарт прода, прямой push в `main`, или изменение машины стадий.
|
||||
|
||||
### AC-A6 — Anti-deadlock / anti-livelock при defer
|
||||
- **PASS:** при занятой сериализации B defer-ится с задержкой и bounded бюджетом; исчерпание
|
||||
бюджета → эскалация (alert/Blocked), не бесконечный цикл и не откат.
|
||||
- **FAIL:** B уходит в вечный defer-цикл, либо немедленно откатывается на `development`.
|
||||
|
||||
### AC-A7 — Условность (не-self репо без регресса)
|
||||
- **PASS:** при выключенном kill-switch и для репо вне scope поведение конвейера 1:1 как до
|
||||
ORCH-026 (нулевая регрессия для enduro-trails).
|
||||
- **FAIL:** не-self репо меняет поведение merge/деплоя.
|
||||
|
||||
---
|
||||
|
||||
## Уровень B — Декларативные зависимости
|
||||
|
||||
### AC-B1 — Декларация зависимости
|
||||
- **PASS:** задача может объявить `blocked-by`/`depends-on` (через выбранный источник —
|
||||
Plane relations / БД / гибрid), и связь корректно считывается планировщиком.
|
||||
- **FAIL:** связь не считывается / теряется.
|
||||
|
||||
### AC-B2 — Гейт планировщика (B не стартует до A)
|
||||
- **PASS:** задача с незавершённым depends-on **не клеймится** воркером (не запускается агент,
|
||||
слот `max_concurrency` не занимается), пока все depends-on не достигли `done`; как только A
|
||||
становится `done` — B становится claimable.
|
||||
- **FAIL:** B запускается раньше завершения A; или занимает слот, простаивая.
|
||||
|
||||
### AC-B3 — Детект дедлоков (циклы)
|
||||
- **PASS:** циклическая зависимость (A→B→A и длиннее) детектируется детерминированно; задача(и)
|
||||
в цикле → `Blocked` + alert (Telegram/Plane) с указанием цикла; поток остальных задач не
|
||||
блокируется.
|
||||
- **FAIL:** цикл приводит к молчаливому вечному ожиданию или к падению воркера.
|
||||
|
||||
### AC-B4 — Видимость заблокированной задачи
|
||||
- **PASS:** заблокированная задача видна — Plane-статус `Blocked` и/или строка ожидания в
|
||||
Telegram-карточке (что/кого ждёт); инвариант «одна карточка на задачу» сохранён.
|
||||
- **FAIL:** заблокированная задача невидима наблюдателю.
|
||||
|
||||
### AC-B5 — Совместимость с reconciler/reaper
|
||||
- **PASS:** `reconciler` F-1 НЕ «разблокирует» задачу, заблокированную по зависимости (как уже
|
||||
делает для Blocked/Needs-Input, ORCH-060/068); reaper не реапит корректно ожидающую задачу.
|
||||
- **FAIL:** reconciler продвигает заблокированную задачу мимо её depends-on.
|
||||
|
||||
---
|
||||
|
||||
## Общие (оба уровня)
|
||||
|
||||
### AC-G1 — never-raise
|
||||
- **PASS:** любая ошибка (git/сеть/БД/Plane) в новой логике не пробрасывается в `advance_stage`/
|
||||
воркер; деградирует консервативно (defer/skip/fail-closed), конвейер не падает.
|
||||
- **FAIL:** необработанное исключение роняет воркер/монитор-поток.
|
||||
|
||||
### AC-G2 — Kill-switch
|
||||
- **PASS:** глобальный kill-switch выключает фичу целиком → поведение 1:1 как до ORCH-026.
|
||||
- **FAIL:** при выключенном флаге поведение изменено.
|
||||
|
||||
### AC-G3 — Документация обновлена (golden source)
|
||||
- **PASS:** в ТОМ ЖЕ PR обновлены `docs/architecture/README.md`, `CLAUDE.md` (если изменилось
|
||||
поведение очереди), `CHANGELOG.md`, заведён ADR в `06-adr/`. Reviewer проверяет.
|
||||
- **FAIL:** код изменён, документация — нет (→ REQUEST_CHANGES).
|
||||
|
||||
### AC-G4 — Миграция БД безопасна (если применимо)
|
||||
- **PASS:** миграция только аддитивная (`CREATE TABLE IF NOT EXISTS`/`_ensure_column`),
|
||||
идемпотентна, безопасна на живой общей прод-БД; существующие данные enduro-trails не затронуты.
|
||||
- **FAIL:** деструктивная миграция / изменение существующих колонок.
|
||||
|
||||
### AC-G5 — Тесты зелёные
|
||||
- **PASS:** новые unit+integration тесты (`04-test-plan.yaml`) проходят; существующий
|
||||
`pytest tests/ -q` остаётся зелёным (нет регресса merge-gate/merge-verify/reconciler/reaper).
|
||||
- **FAIL:** красный pytest или регресс существующих тестов.
|
||||
169
docs/work-items/ORCH-026/04-test-plan.yaml
Normal file
169
docs/work-items/ORCH-026/04-test-plan.yaml
Normal file
@@ -0,0 +1,169 @@
|
||||
work_item: ORCH-026
|
||||
description: >
|
||||
План тестов для управления зависимостями задач (Уровень B) и сериализации
|
||||
merge/деплоя внутри одного репо (Уровень A). Стек: pytest. Имена модулей/функций —
|
||||
кандидаты; финализирует архитектор/разработчик. Все новые функции — never-raise.
|
||||
|
||||
tests:
|
||||
# ---------------- Уровень A: сериализация merge/деплоя ----------------
|
||||
- id: TC-A01
|
||||
type: unit
|
||||
description: >
|
||||
Proactive pre-merge rebase: ветка догоняется на свежий origin/main ДАЖЕ когда
|
||||
branch_is_behind_main вернул бы False (нет конфликта). Проверить, что rebase
|
||||
вызывается всегда перед merge (AC-A2).
|
||||
module: tests/test_orch026_premerge_rebase.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-A02
|
||||
type: unit
|
||||
description: >
|
||||
Расширенное окно merge-lease: пока A держит lease (окно merge→main-updated),
|
||||
acquire для B того же репо возвращает busy → defer (не откат). holder-aware
|
||||
release не удаляет чужой lease (AC-A1, AC-A6).
|
||||
module: tests/test_orch026_merge_serialize.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-A03
|
||||
type: unit
|
||||
description: >
|
||||
Сериализация строго per-repo: lease/гейт orchestrator не влияет на задачу
|
||||
enduro-trails — обе claimable параллельно (AC-A3).
|
||||
module: tests/test_orch026_merge_serialize.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-A04
|
||||
type: unit
|
||||
description: >
|
||||
Restart-safe + проактивный реклейм: мёртвый держатель lease (pid не жив)
|
||||
реклеймится reclaim_stale_lease; конвейер не встаёт навсегда (AC-A4).
|
||||
module: tests/test_orch026_merge_serialize.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-A05
|
||||
type: unit
|
||||
description: >
|
||||
Anti-livelock defer: B defer-ится с available_at-задержкой и bounded бюджетом;
|
||||
исчерпание → эскалация (Blocked/alert), не бесконечный цикл (AC-A6).
|
||||
module: tests/test_orch026_merge_serialize.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-A06
|
||||
type: unit
|
||||
description: >
|
||||
Условность/kill-switch: при выключенном флаге и для репо вне scope поведение
|
||||
merge/деплоя 1:1 как до ORCH-026 — no-op (AC-A7, AC-G2).
|
||||
module: tests/test_orch026_conditionality.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-A07
|
||||
type: unit
|
||||
description: >
|
||||
Self-hosting safety: новая логика никогда не делает push/force-push в main;
|
||||
force только --force-with-lease на ветку задачи; STAGE_TRANSITIONS не изменены
|
||||
(AC-A5).
|
||||
module: tests/test_orch026_conditionality.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-A08
|
||||
type: integration
|
||||
description: >
|
||||
Сквозной сценарий: две задачи одного репо проходят deploy-staging→deploy; B не
|
||||
доходит до merge, пока A не в main; после A→done B ребейзится на свежий main
|
||||
(несёт код A) и мержится. main не теряет код A (AC-A1/AC-A2).
|
||||
module: tests/test_orch026_serialize_integration.py
|
||||
expected: PASS
|
||||
|
||||
# ---------------- Уровень B: декларативные зависимости ----------------
|
||||
- id: TC-B01
|
||||
type: unit
|
||||
description: >
|
||||
Чтение/декларация связи blocked-by из выбранного источника (Plane/БД/гибрид);
|
||||
связь корректно резолвится в depends_on_task_id (AC-B1). never-raise при
|
||||
недоступности источника → консервативно (нет связи или fail-closed по решению ADR).
|
||||
module: tests/test_orch026_task_deps.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-B02
|
||||
type: unit
|
||||
description: >
|
||||
Гейт готовности (leaf-функция): задача с незавершённым depends-on НЕ ready;
|
||||
все depends-on в done → ready. Чистая логика, юнит-тестируемая (AC-B2).
|
||||
module: tests/test_orch026_task_deps.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-B03
|
||||
type: unit
|
||||
description: >
|
||||
Детект циклов: A→B→A (и длиннее) детектируется детерминированно; ацикличный
|
||||
граф → циклов нет. Чистая функция (AC-B3).
|
||||
module: tests/test_orch026_dep_cycles.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-B04
|
||||
type: unit
|
||||
description: >
|
||||
Цикл → set_issue_blocked + alert (Telegram/Plane), без падения воркера и без
|
||||
блокировки потока других задач (AC-B3, AC-G1).
|
||||
module: tests/test_orch026_dep_cycles.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-B05
|
||||
type: unit
|
||||
description: >
|
||||
claim_next_job не клеймит заблокированную задачу (не занимает слот
|
||||
max_concurrency); как только depends-on done — задача становится claimable (AC-B2).
|
||||
module: tests/test_orch026_task_deps.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-B06
|
||||
type: unit
|
||||
description: >
|
||||
Видимость: заблокированная задача отражается в Plane-статусе Blocked и/или
|
||||
строке ожидания Telegram-карточки; инвариант «одна карточка на задачу» сохранён
|
||||
(AC-B4). notifications never-raise / silent.
|
||||
module: tests/test_orch026_dep_visibility.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-B07
|
||||
type: unit
|
||||
description: >
|
||||
reconciler F-1 НЕ разблокирует задачу, заблокированную по зависимости (как для
|
||||
Blocked/Needs-Input); reaper не реапит корректно ожидающую (AC-B5).
|
||||
module: tests/test_orch026_task_deps.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-B08
|
||||
type: integration
|
||||
description: >
|
||||
Сквозной сценарий: B объявлена blocked-by A; при постановке в очередь B не
|
||||
стартует, пока A не done; после A→done воркер запускает B. Telegram/Plane
|
||||
показывают Blocked у B до разблокировки (AC-B1/B2/B4).
|
||||
module: tests/test_orch026_deps_integration.py
|
||||
expected: PASS
|
||||
|
||||
# ---------------- Общие / миграция / регресс ----------------
|
||||
- id: TC-G01
|
||||
type: unit
|
||||
description: >
|
||||
Аддитивная миграция БД (если выбран вариант с таблицей/колонкой): идемпотентна,
|
||||
безопасна на существующей БД с данными, не меняет существующие колонки (AC-G4).
|
||||
module: tests/test_orch026_migration.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-G02
|
||||
type: unit
|
||||
description: >
|
||||
Наблюдаемость GET /queue: новый блок (заблокированные задачи / держатель lease /
|
||||
defer-счётчики / циклы) присутствует и read-only; не источник истины.
|
||||
module: tests/test_orch026_queue_observability.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-G03
|
||||
type: integration
|
||||
description: >
|
||||
Регресс: полный pytest tests/ -q остаётся зелёным — merge-gate (ORCH-043),
|
||||
merge-verify (ORCH-073), reconciler (ORCH-053/068), reaper (ORCH-065) не
|
||||
деградировали (AC-G5).
|
||||
module: tests/
|
||||
expected: PASS
|
||||
@@ -0,0 +1,226 @@
|
||||
# ADR-001: Сериализация merge/деплоя внутри репо (A) + декларативные зависимости задач (B)
|
||||
|
||||
**Work Item:** ORCH-026 · **Repo:** orchestrator (self-hosting) · **Стадия:** architecture
|
||||
**Статус:** Accepted
|
||||
**Связи:** дополняет ORCH-043 (merge-gate), ORCH-065 (merge-lease + reclaim), ORCH-073/071
|
||||
(merge-verify, SHA-in-main), ORCH-1 (очередь). Глобальный ADR — `adr/adr-0015`.
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
ORCH-026 закрывает **первопричину** эрозии `main` 08.06 (некоординированный параллелизм
|
||||
задач одного репо: ветки от устаревшего `main`, фантом-merge затирает соседа) и попутно вводит
|
||||
исходный скоуп — декларативные зависимости задач (B ждёт A). Требования — `01-brd.md`,
|
||||
`02-trz.md`; PASS/FAIL — `03-acceptance-criteria.md`.
|
||||
|
||||
Ключевое наблюдение архитектора: **бо́льшая часть инфраструктуры для Уровня A уже существует** и
|
||||
её НЕ нужно переписывать:
|
||||
|
||||
- **merge-lease** (ORCH-065, `src/merge_gate.py`): per-repo файловый лиз
|
||||
`.merge-lease-<repo>.json`, неблокирующий acquire, holder-aware release, проактивный реклейм
|
||||
мёртвого/устаревшего держателя (`reclaim_stale_lease`, `pid_alive`). Restart-safe, per-repo.
|
||||
- **merge-gate** (ORCH-043, `check_branch_mergeable`): на ребре `deploy-staging → deploy`
|
||||
захватывает лиз, при необходимости ребейзит, держит лиз до фактического merge.
|
||||
- **defer-механизм** (`_handle_merge_gate_defer`): `merge-lock busy` → повторная постановка
|
||||
deployer'а через `available_at`, bounded `merge_defer_max_attempts` → эскалация (Blocked+alert).
|
||||
- **окно лиза** уже простирается от `deploy-staging → deploy` до release на одном из событий:
|
||||
PR-merged webhook (`gitea.py`), `deploy→done` (`stage_engine.py`), откат, проактивный реклейм.
|
||||
Для self-hosting `done` достигается ТОЛЬКО после `verify_merged_to_main` (SHA-in-main, ORCH-073).
|
||||
|
||||
Таким образом окно сериализации A-1 («merge → main-updated») **структурно уже реализовано**:
|
||||
пока A не подтверждена в `main` (для self — SHA-in-main → `done`), лиз держится, и B того же
|
||||
репо на своём merge-gate получает `merge-lock busy` → defer. Открытый вопрос BRD §4.3 (граница
|
||||
окна для self) решается так: **окно = от acquire до release; release-события не меняем**. Для
|
||||
non-self репо граница — PR-merged webhook; для self — `deploy→done` (= SHA-in-main подтверждён).
|
||||
|
||||
Что реально **отсутствует** для Уровня A:
|
||||
|
||||
- **A-2: безусловный proactive pre-merge rebase.** Сейчас `check_branch_mergeable` ребейзит
|
||||
ТОЛЬКО если `branch_is_behind_main` (⇔ `origin/main` не предок HEAD). AC-A2 требует, чтобы
|
||||
rebase вызывался **всегда** перед merge — детерминированный структурный анти-фантом на уровне
|
||||
планировщика, не зависящий от точности ancestor-проверки.
|
||||
|
||||
Для Уровня B инфраструктуры нет вовсе: очередь `jobs` (ORCH-1) плоская (FIFO по `id` +
|
||||
`available_at` + `max_concurrency`), выразить «B ждёт A» нельзя.
|
||||
|
||||
---
|
||||
|
||||
## Решение
|
||||
|
||||
### Уровень A — сериализация merge/деплоя (минимально-инвазивно, переиспользуя ORCH-043/065)
|
||||
|
||||
**A-1/A-3/A-4 (окно сериализации) — без изменений механизма.** Окно сериализации обеспечивается
|
||||
существующим merge-lease: захват в `check_branch_mergeable`, удержание до release. Подтверждаем и
|
||||
фиксируем в доке, что release-события (`PR-merged` / `deploy→done` / откат / `reclaim_stale_lease`)
|
||||
формируют окно «merge → main-updated». Кросс-репо параллелизм сохранён автоматически (лиз —
|
||||
per-repo файл). Restart-safe и анти-залипание — за счёт ORCH-065 reclaim. **Кода-изменений нет.**
|
||||
|
||||
**A-2 (безусловный pre-merge rebase) — новое поведение, флаг `premerge_rebase_always`.**
|
||||
|
||||
- В `check_branch_mergeable` (`src/qg/checks.py`), ПОД захваченным merge-lease: когда
|
||||
`settings.premerge_rebase_always` истинно (и merge-gate применим к репо), **пропустить
|
||||
short-circuit `branch_is_behind_main`** и **всегда** вызвать `merge_gate.auto_rebase_onto_main`.
|
||||
- `auto_rebase_onto_main` уже идемпотентен и дёшев на актуальной ветке: `git rebase origin/main`
|
||||
на не-отстающей ветке — no-op (rc 0, HEAD не меняется), последующий `push --force-with-lease`
|
||||
→ «Everything up-to-date» (тот же SHA, **CI не перезапускается, лишних коммитов нет**). На
|
||||
отстающей ветке — реальный догон. Текстовый конфликт → существующий контракт: `rebase --abort`
|
||||
→ откат на `development` (как ORCH-043). **Инвариант: никаких push/force-push в `main`** —
|
||||
единственная force-операция остаётся `--force-with-lease` на ветку задачи.
|
||||
- Когда флаг выключен → прежнее поведение (ребейз только при `branch_is_behind_main`),
|
||||
обратная совместимость 1:1 (AC-A7/AC-G2).
|
||||
- **Скоуп — общий с merge-gate:** реально только для `merge_gate_repos` (пусто → self-hosting
|
||||
`orchestrator`). Никакого нового scope-флага.
|
||||
|
||||
**A-5/A-6 (safety, anti-livelock) — без изменений.** `STAGE_TRANSITIONS`, `QG_CHECKS`,
|
||||
`Confirm Deploy` (ORCH-059), exit-коды хука, terminal-sync не трогаются. defer-бюджет —
|
||||
существующий `merge_defer_max_attempts` → Blocked+alert при исчерпании. Прод-контейнер не
|
||||
рестартится вне штатного `Confirm Deploy`.
|
||||
|
||||
### Уровень B — декларативные зависимости (новая инфраструктура)
|
||||
|
||||
**B-источник: гибрид с БД как источником истины для планировщика; флаг `task_deps_source`.**
|
||||
|
||||
Планировщик `claim_next_job` — горячий цикл, обслуживающий очередь ВСЕХ проектов из ОДНОГО
|
||||
инстанса. Он **обязан** быть offline-устойчивым и быстрым: сетевой запрос в Plane на каждый claim
|
||||
= при недоступности Plane встанет конвейер всех проектов (нарушение self-hosting safety). Поэтому:
|
||||
|
||||
- **Авторитетный для планировщика стор — локальная БД**, новая аддитивная таблица
|
||||
`job_deps(task_id, depends_on_task_id, created_at)` (детали — `08-data-requirements.md`).
|
||||
Связь хранится по `tasks.id` (стабильный локальный ключ). Зависимости — **только внутри одного
|
||||
репо** (v1; кросс-репо — non-goal, BRD §5).
|
||||
- **`task_deps_source = db | plane | hybrid`** (дефолт **`db`**): `db` — связи пишутся напрямую в
|
||||
`job_deps` (потребитель — декомпозиция эпиков ORCH-025); `plane` — связи читаются из Plane
|
||||
relations в `handle_work_item_created` и **кэшируются** в `job_deps`; `hybrid` — Plane как
|
||||
декларация + БД-кэш. Plane-ingestion — тонкий add-on за флагом; планировщик ВСЕГДА читает БД.
|
||||
|
||||
**B-2 (гейт планировщика) — SQL `NOT EXISTS`, без занятия слота `max_concurrency`.**
|
||||
|
||||
Гейт готовности выражается декларативно в `claim_next_job` (`src/db.py`): задача claimable, если
|
||||
у неё нет ни одной незавершённой зависимости. Когда `settings.task_deps_enabled` — к существующему
|
||||
SELECT добавляется условие:
|
||||
|
||||
```sql
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM job_deps d
|
||||
JOIN tasks t ON t.id = d.depends_on_task_id
|
||||
WHERE d.task_id = j.task_id AND t.stage != 'done'
|
||||
)
|
||||
```
|
||||
|
||||
Это: (1) **не занимает слот** — job просто не выбирается, агент не запускается (AC-B2);
|
||||
(2) restart-safe (чистая БД); (3) never-raise (это SQL); (4) при пустой `job_deps` —
|
||||
инертно (нулевая регрессия, AC-G2); (5) при выключенном `task_deps_enabled` условие НЕ
|
||||
добавляется → запрос 1:1 как в ORCH-1. Как только все зависимости достигают `stage='done'`,
|
||||
задача автоматически становится claimable.
|
||||
|
||||
Чистая leaf-логика «готова ли задача» выносится в новый модуль `src/task_deps.py`:
|
||||
`is_task_ready(task_id) -> (bool, waiting_on: list[str])` (never-raise) — для реконсилятора,
|
||||
карточки и `/queue` (SQL в `claim_next_job` — горячий путь, дублирует ту же семантику).
|
||||
|
||||
**B-3 (детект дедлоков) — DFS, чистая функция.**
|
||||
|
||||
`task_deps.detect_cycle(task_id) -> list[int] | None` — обход графа `job_deps` (внутри репо),
|
||||
детерминированный, юнит-тестируемый, never-raise. Запускается: (1) при вставке связи
|
||||
(`add_dependency`) — цикл отклоняется/алертится сразу (лучший UX); (2) backstop-проход в тике
|
||||
`reconciler` (на случай связей, добавленных в обход). Цикл → `set_issue_blocked(work_item_id)` +
|
||||
Telegram/Plane alert с перечислением цикла. SQL-гейт B-2 сам по себе никогда не выберет задачу в
|
||||
цикле (её зависимости не достигнут `done`) — детектор делает это **видимым**, а не молчаливым
|
||||
вечным ожиданием (AC-B3). Поток остальных задач не блокируется.
|
||||
|
||||
**B-4 (видимость).**
|
||||
|
||||
- Нормальное ожидание (B ждёт A, A в работе — транзиентно и ожидаемо): строка в Telegram-карточке
|
||||
«⏳ ждёт ORCH-NNN» через `notifications.update_task_tracker`, never-raise/silent. **Plane Blocked
|
||||
при нормальном ожидании НЕ ставим** — иначе флаппинг Blocked на каждом коротком ожидании.
|
||||
- Дедлок/цикл (B-3): `set_issue_blocked` (Plane `Blocked`) + alert. Это «и/или» из AC-B4.
|
||||
- Инвариант «одна карточка на задачу» сохранён (ORCH-042/067).
|
||||
|
||||
**B-5 (совместимость reconciler/reaper).**
|
||||
|
||||
- `reconciler` F-1 не должен «разблокировать» dep-заблокированную задачу мимо её зависимостей.
|
||||
В фильтр пригодности reconciler добавляется проверка `task_deps.is_task_ready` (по образцу
|
||||
`reconcile_skip_blocked_enabled`, ORCH-060): не готова → skip.
|
||||
- `reaper` сканирует **`running`** jobs; dep-заблокированный job остаётся `queued` (его не
|
||||
клеймят) → reaper его не трогает по построению. Фиксируем в доке.
|
||||
|
||||
**Наблюдаемость (TRZ §4):** блок `task_deps` в снимке `GET /queue` (read-only, по образцу
|
||||
`reconcile`/`reaper`): кол-во заблокированных задач, держатель merge-lease, defer-счётчики,
|
||||
обнаруженные циклы. Никогда не источник решений.
|
||||
|
||||
### Конфигурация (`src/config.py`)
|
||||
|
||||
| Флаг | Дефолт | Назначение |
|
||||
|------|--------|-----------|
|
||||
| `premerge_rebase_always` | `True` | Уровень A: безусловный pre-merge rebase под лизом. Скоуп — `merge_gate_repos`. Kill-switch (`False` → ребейз только при behind, как ORCH-043). |
|
||||
| `task_deps_enabled` | `True` | Уровень B: глобальный kill-switch гейта зависимостей. `False` → `claim_next_job` 1:1 как ORCH-1. Инертно при пустой `job_deps`. |
|
||||
| `task_deps_source` | `"db"` | Источник деклараций: `db`\|`plane`\|`hybrid`. Планировщик всегда читает БД-кэш. |
|
||||
|
||||
Дефолты следуют конвенции репо (`*_enabled=True` + kill-switch), при этом обе фичи инертны без
|
||||
данных (нет деклараций / нет применимых репо) → нулевая регрессия для enduro-trails.
|
||||
|
||||
---
|
||||
|
||||
## Альтернативы (и почему отвергнуты)
|
||||
|
||||
1. **Уровень A — отдельный глобальный per-repo merge-lock или FIFO merge-queue.** Дублировал бы
|
||||
уже существующий merge-lease (ORCH-065), вводил второй механизм сериализации с риском
|
||||
рассинхрона. Отвергнуто: BRD §4.2 требует минимально-инвазивного решения, переиспользующего
|
||||
ORCH-065/043. Окно лиза уже даёт сериализацию.
|
||||
|
||||
2. **Уровень A — расширять release-точки лиза (держать до отдельного `main-updated`-события).**
|
||||
Не требуется: для self `done` ⇔ SHA-in-main (ORCH-073), для non-self — PR-merged webhook;
|
||||
окно уже корректно. Доп. событие усложнило бы reclaim без выигрыша.
|
||||
|
||||
3. **Уровень B — Plane relations как источник истины планировщика.** Сетевой запрос в горячем
|
||||
цикле claim; при недоступности Plane встаёт очередь всех проектов (self-hosting risk).
|
||||
Отвергнуто; Plane оставлен опциональным источником **декларации** (`task_deps_source=plane`),
|
||||
но планировщик читает только БД-кэш.
|
||||
|
||||
4. **Уровень B — гейт зависимостей в воркере (`_drain_once`) поверх `claim_next_job`.** Пришлось
|
||||
бы клеймить job, обнаруживать незавершённую зависимость и re-queue’ить — churn, расход attempts,
|
||||
гонки. SQL `NOT EXISTS` в самом `claim_next_job` чище: job просто не выбирается, слот свободен.
|
||||
|
||||
5. **Уровень B — поле/JSON в `tasks` вместо таблицы.** Таблица `job_deps` нормальна (M:N),
|
||||
индексируема, проще для DFS и `NOT EXISTS`. Поле в `tasks` потребовало бы парсинг-логики.
|
||||
|
||||
---
|
||||
|
||||
## Последствия
|
||||
|
||||
**Плюсы.**
|
||||
- Минимально-инвазивно: Уровень A — один флаг + снятие short-circuit; окно сериализации не
|
||||
переписывается. Переиспользует ORCH-043/065 целиком.
|
||||
- Уровень B — одно `NOT EXISTS` в `claim_next_job` + аддитивная таблица + leaf-модуль
|
||||
`task_deps.py`; `STAGE_TRANSITIONS`/`QG_CHECKS` не тронуты (паттерн врезки ORCH-071/058).
|
||||
- Обе фичи инертны без данных → нулевая регрессия для enduro-trails (AC-A7/AC-G2).
|
||||
- restart-safe (БД + файловый лиз), never-raise, kill-switch на каждую фичу.
|
||||
|
||||
**Минусы / ограничения.**
|
||||
- `premerge_rebase_always=True` добавляет (дешёвый, no-op на актуальной ветке) `rebase`+`push`
|
||||
на каждый self-merge. Цена — лишний git-вызов; компенсируется детерминизмом анти-фантома.
|
||||
- Уровень B v1 — только intra-repo зависимости; кросс-репо — follow-up (non-goal).
|
||||
- Гейт B-2 в `claim_next_job` слегка усложняет горячий SQL (один `NOT EXISTS`); защищён
|
||||
kill-switch и инертностью при пустой таблице.
|
||||
- `task_deps.py` цикл-детектор — новая поверхность; покрывается юнит-тестами (`04-test-plan.yaml`).
|
||||
|
||||
**Инварианты (не нарушать).**
|
||||
1. `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`/`check_staging_status`,
|
||||
`Confirm Deploy` (ORCH-059), БАГ-8, terminal-sync — без изменений.
|
||||
2. Никаких push/force-push в `main`; force только `--force-with-lease` на ветку задачи.
|
||||
3. Сериализация — строго per-repo; кросс-репо параллелизм сохранён.
|
||||
4. never-raise во всех новых функциях; restart-safe состояние; миграция БД только аддитивная.
|
||||
5. ORCH-026 **дополняет** рубежи ORCH-073, не заменяет.
|
||||
6. Прод-контейнер orchestrator не рестартится вне штатного `Confirm Deploy`.
|
||||
|
||||
**Места реализации (для developer).**
|
||||
- `src/qg/checks.py::check_branch_mergeable` — ветка `premerge_rebase_always`.
|
||||
- `src/db.py::claim_next_job` — условный `NOT EXISTS`-гейт; новые helpers `add_dependency`,
|
||||
`get_dependencies`, `job_deps` миграция в `init_db` (`CREATE TABLE IF NOT EXISTS`).
|
||||
- `src/task_deps.py` (новый, leaf) — `is_task_ready`, `detect_cycle`, snapshot для `/queue`.
|
||||
- `src/webhooks/plane.py::handle_work_item_created` — ingestion Plane relations (за `task_deps_source`).
|
||||
- `src/reconciler.py` — skip dep-заблокированных + backstop цикл-детект.
|
||||
- `src/notifications.py` — строка ожидания в карточке.
|
||||
- `src/config.py` — `premerge_rebase_always`, `task_deps_enabled`, `task_deps_source`.
|
||||
- Документация: `docs/architecture/README.md`, `CLAUDE.md` (если меняется поведение очереди),
|
||||
`CHANGELOG.md`, глобальный `adr/adr-0015`.
|
||||
65
docs/work-items/ORCH-026/08-data-requirements.md
Normal file
65
docs/work-items/ORCH-026/08-data-requirements.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# 08 — Требования к схеме БД — ORCH-026
|
||||
|
||||
**Work Item:** ORCH-026 · **Repo:** orchestrator · **Стадия:** architecture
|
||||
**Связь:** ADR `06-adr/ADR-001-merge-serialization-and-task-deps.md` (Уровень B).
|
||||
|
||||
> Уровень A (сериализация merge/деплоя) — **БЕЗ изменения схемы БД** (merge-lease файловый,
|
||||
> `.merge-lease-<repo>.json`, ORCH-065). Изменения схемы касаются ТОЛЬКО Уровня B.
|
||||
|
||||
---
|
||||
|
||||
## Новая таблица `job_deps` (аддитивная)
|
||||
|
||||
Хранит декларативные зависимости «задача `task_id` ждёт задачу `depends_on_task_id`».
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS job_deps (
|
||||
task_id INTEGER NOT NULL, -- tasks.id зависимой задачи (B)
|
||||
depends_on_task_id INTEGER NOT NULL, -- tasks.id задачи-предшественника (A)
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (task_id, depends_on_task_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_job_deps_task ON job_deps(task_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_job_deps_depends ON job_deps(depends_on_task_id);
|
||||
```
|
||||
|
||||
### Поля
|
||||
| Поле | Тип | Назначение |
|
||||
|------|-----|-----------|
|
||||
| `task_id` | INTEGER | `tasks.id` зависимой задачи (B). Не запускается, пока зависимости не `done`. |
|
||||
| `depends_on_task_id` | INTEGER | `tasks.id` предшественника (A). Терминальность — `tasks.stage = 'done'`. |
|
||||
| `created_at` | TEXT | Время декларации (диагностика). |
|
||||
|
||||
### Ключ и индексы
|
||||
- **PK `(task_id, depends_on_task_id)`** — идемпотентность вставки (повторная декларация связи —
|
||||
no-op через `INSERT OR IGNORE`), запрет дублей.
|
||||
- `idx_job_deps_task` — гейт планировщика (`NOT EXISTS ... WHERE d.task_id = j.task_id`).
|
||||
- `idx_job_deps_depends` — обратные рёбра для DFS цикл-детектора.
|
||||
|
||||
### Семантика готовности (источник истины планировщика)
|
||||
Задача `task_id` **готова к запуску** ⇔ нет ни одной строки `job_deps` для неё, чей
|
||||
`depends_on_task_id` указывает на задачу с `tasks.stage != 'done'`. Терминал — только `done`
|
||||
(совпадает с тем, как `get_active_tasks_for_reconcile` трактует терминальность).
|
||||
|
||||
### Связь по `task_id`, а не `work_item_id`
|
||||
`tasks.id` — стабильный локальный автоинкремент-ключ; `work_item_id`/`plane_id` могут
|
||||
ресолвиться/коллизиться (см. `ensure_unique_work_item_id`). FK логический (без `REFERENCES`,
|
||||
как у `jobs.task_id`) — не блокирует аддитивную миграцию и удаление строк tasks (которого в
|
||||
конвейере нет). Зависимости — **только intra-repo** (v1); кросс-репо рёбра не создаются.
|
||||
|
||||
---
|
||||
|
||||
## Миграция (AC-G4)
|
||||
|
||||
- Выполняется в `src/db.py::init_db` рядом с прочими: **только** `CREATE TABLE IF NOT EXISTS` +
|
||||
`CREATE INDEX IF NOT EXISTS`. **Идемпотентно**, restart-safe, безопасно на живой общей прод-БД.
|
||||
- **Существующие колонки/таблицы (`jobs`, `tasks`, `agent_runs`, `events`) НЕ изменяются** →
|
||||
данные enduro-trails не затронуты.
|
||||
- Откат фичи — флагом `task_deps_enabled=False` (таблица остаётся, гейт не применяется); сама
|
||||
таблица деструктивно не удаляется.
|
||||
|
||||
## Что НЕ меняется
|
||||
- Схема `jobs` (включая `available_at`, `pid`, `attempts`/`transient_attempts`) — без изменений;
|
||||
defer Уровня A/B переиспользует существующий `available_at`-механизм.
|
||||
- Схема `tasks` — без изменений (видимость через существующие `tracker_message_id` и Plane Blocked).
|
||||
- merge-lease — файловый, вне БД.
|
||||
17
docs/work-items/ORCH-026/10-tech-risks.md
Normal file
17
docs/work-items/ORCH-026/10-tech-risks.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# 10 — Технические риски — ORCH-026
|
||||
|
||||
**Work Item:** ORCH-026 · **Repo:** orchestrator · **Стадия:** architecture
|
||||
**Связь:** ADR `06-adr/ADR-001-merge-serialization-and-task-deps.md`.
|
||||
|
||||
| # | Риск | Уровень | Митигация |
|
||||
|---|------|---------|-----------|
|
||||
| R-1 | **Гейт `NOT EXISTS` в `claim_next_job` (горячий путь всех проектов) содержит баг → встаёт очередь ВСЕХ проектов** (self-hosting групповой риск). | Высокий | Условие добавляется ТОЛЬКО при `task_deps_enabled`; инертно при пустой `job_deps` (нулевая регрессия); kill-switch `task_deps_enabled=False` мгновенно возвращает поведение ORCH-1; интеграционный тест «пустые deps ⇒ FIFO 1:1» (AC-G2). |
|
||||
| R-2 | **Безусловный `premerge_rebase_always` делает лишний `push --force-with-lease` → ложный перезапуск CI / новые коммиты.** | Низкий | На актуальной ветке `rebase origin/main` — no-op (HEAD не меняется), push → «Everything up-to-date» (тот же SHA, CI не триггерится). Подтвердить тестом, что SHA не меняется на уже-актуальной ветке. |
|
||||
| R-3 | **Дедлок по циклической зависимости → задача молча ждёт вечно.** | Средний | DFS-детектор `detect_cycle` при вставке связи + backstop в `reconciler`; цикл → `set_issue_blocked` + alert с перечислением цикла (AC-B3); SQL-гейт не выбирает задачу в цикле, детектор делает это видимым. |
|
||||
| R-4 | **Livelock: B бесконечно defer’ится на `merge-lock busy`.** | Низкий | Существующий bounded-бюджет `merge_defer_max_attempts` → Blocked+alert (ORCH-043, без изменений). |
|
||||
| R-5 | **Залипший merge-lease после смерти держателя → конвейер репо встаёт навсегда.** | Средний | Переиспользуется ORCH-065: `reclaim_stale_lease` (мёртвый `pid` / TTL `merge_lock_timeout_s`) + holder-aware release. Restart-safe (AC-A4). |
|
||||
| R-6 | **Plane relations недоступны/неверно смаплены при `task_deps_source=plane`.** | Средний | Планировщик читает ТОЛЬКО БД-кэш `job_deps`; Plane-ingestion — best-effort, never-raise; дефолт `task_deps_source=db` не зависит от Plane. |
|
||||
| R-7 | **reconciler «разблокирует» dep-заблокированную задачу мимо её зависимостей.** | Средний | В фильтр reconciler добавляется `is_task_ready` (паттерн ORCH-060 skip-Blocked); reaper трогает только `running` — dep-блок остаётся `queued` (AC-B5). |
|
||||
| R-8 | **Миграция БД повреждает общую прод-БД (данные enduro-trails).** | Низкий | Только аддитивно: `CREATE TABLE/INDEX IF NOT EXISTS`; существующие колонки не меняются; идемпотентно (AC-G4). |
|
||||
| R-9 | **Self-hosting: изменения требуют рестарта прод-контейнера вне `Confirm Deploy`.** | Высокий (если нарушено) | Все изменения — обычный код, проходят `deploy-staging` (8501) → `Confirm Deploy` (ORCH-059). `STAGE_TRANSITIONS`/`QG_CHECKS` не трогаются; никакого внеочередного рестарта (AC-A5). |
|
||||
| R-10 | **Конфликт точек интеграции A (merge-gate) и B (постановка в очередь).** | Низкий | Разные точки конвейера: B гейтит claim job (вход), A гейтит merge на ребре `deploy-staging→deploy`. Независимы; покрыть интеграционным тестом совместной работы (BRD §4.4). |
|
||||
47
docs/work-items/ORCH-026/12-review.md
Normal file
47
docs/work-items/ORCH-026/12-review.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-026
|
||||
verdict: APPROVED
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-026
|
||||
|
||||
## Summary
|
||||
ORCH-026 реализует два уровня по ADR-001: **Уровень A** — сериализация merge/deploy внутри одного репо (переиспользует merge-lease ORCH-043/065 + единственная новая логика — безусловный pre-merge rebase под флагом `premerge_rebase_always`) и **Уровень B** — декларативные зависимости задач (аддитивная таблица `job_deps`, гейт `NOT EXISTS` в `claim_next_job`, leaf-модуль `src/task_deps.py`). Реализация минимально-инвазивна, строго соответствует ТЗ и ADR, обе фичи условны (kill-switch) и инертны без данных. Все 16 критериев приёмки выполнены. Полный прогон `pytest tests/ -q` — **991 passed**, из них 50 новых ORCH-026-тестов зелёные. Документация обновлена в том же PR. **APPROVED.**
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- (нет)
|
||||
|
||||
### P1 — Must fix
|
||||
- (нет)
|
||||
|
||||
### P2 — Should fix
|
||||
- (нет)
|
||||
|
||||
### P3 — Nice to have
|
||||
- [ ] PR-ветка несёт коммиты ORCH-073 (`main` ещё не получил merge #77, merge-base = `77abfb3`). Это ожидаемо по топологии (ORCH-026 (B) построен поверх уже отревьюенного предшественника ORCH-073 (A): у ORCH-073 есть собственные `12-review.md`/`13-test-report.md`/`14-deploy-log.md`) и фактически демонстрирует саму фичу A (rebase B на код A). Не блокирует; при merge в `main` приедут оба набора изменений — это корректно.
|
||||
|
||||
## Соответствие ТЗ и ADR
|
||||
- **Уровень A (AC-A1…A7):** окно сериализации обеспечено существующим merge-lease без нового механизма (ADR §A-1/A-3/A-4). A-2 — `check_branch_mergeable` (`src/qg/checks.py`) под лизом при `premerge_rebase_always=True` всегда вызывает `auto_rebase_onto_main`, снимая short-circuit `branch_is_behind_main`; kill-switch off → поведение ORCH-043 1:1. `STAGE_TRANSITIONS`/`QG_CHECKS`/`Confirm Deploy` не тронуты — соответствует инвариантам §9. Никаких push/force в `main` (только `--force-with-lease` ветки).
|
||||
- **Уровень B (AC-B1…B5):** гейт `NOT EXISTS (job_deps JOIN tasks WHERE stage!='done')` в `claim_next_job` (`src/db.py`) — job не выбирается, слот `max_concurrency` не занимается; при выключенном флаге / пустой таблице clause не добавляется (нулевая регрессия). `task_deps.py` — чистый leaf: `is_task_ready` (fail-open), итеративный WHITE/GREY/BLACK DFS-детектор циклов (защита от recursion-limit на проде), `handle_cycle` (Blocked+alert), `declare_dependency`, `ingest_plane_relations` (только `plane|hybrid`, дефолт `db` не ходит в сеть на горячем пути). reconciler F-1 получил Guard 3 (skip dep-заблокированных + backstop детект цикла); reaper не тронут (сканирует `running`).
|
||||
- **Общие (AC-G1…G5):** контракт never-raise выдержан во всех новых функциях (try/except, консервативная деградация). Миграция строго аддитивна — `CREATE TABLE/INDEX IF NOT EXISTS`, без `REFERENCES`, схема `tasks`/`jobs` не изменена (AC-G4 OK на живой общей БД). Наблюдаемость — read-only блок `task_deps` в `GET /queue`. Реализация в точности по местам, указанным в ADR §«Места реализации».
|
||||
|
||||
## Качество кода
|
||||
- Docstrings на всех публичных функциях, явно документирован контракт fail-open/fail-closed.
|
||||
- SQL-гейт безопасен: `dep_gate` — константная строка (нет инъекции), таблица `job_deps` гарантированно создана в `init_db`.
|
||||
- Переменные `plane_id`/`plane_project_id`/`task_id` в `start_pipeline` — в области видимости (проверено).
|
||||
- Тесты содержательные: миграция, conditionality (kill-switch), циклы, видимость, observability, интеграция сериализации и зависимостей.
|
||||
|
||||
## Документация — обновлена (golden source)
|
||||
Проверено: код в `src/` изменён → документация обновлена В ТОМ ЖЕ PR (разнесена по pipeline-коммитам ветки, что нормально):
|
||||
- `docs/architecture/README.md` — разделы про очередь (`claim_next_job`-гейт), pre-merge rebase, «Зависимости задач: B ждёт A», `job_deps`, наблюдаемость (architect-коммит `f8ec1c2`). ✓
|
||||
- `docs/work-items/ORCH-026/06-adr/ADR-001-merge-serialization-and-task-deps.md` + глобальный `docs/architecture/adr/adr-0015-task-deps-and-merge-serialization.md`. ✓
|
||||
- `CLAUDE.md` — паспорт (очередь/сериализация). ✓
|
||||
- `CHANGELOG.md` — запись `## [Unreleased]`. ✓
|
||||
- `.env.example` — `ORCH_PREMERGE_REBASE_ALWAYS`/`ORCH_TASK_DEPS_ENABLED`/`ORCH_TASK_DEPS_SOURCE`. ✓
|
||||
- `08-data-requirements.md` — таблица `job_deps`. ✓
|
||||
|
||||
Документация = golden source: требование выполнено.
|
||||
75
docs/work-items/ORCH-026/13-test-report.md
Normal file
75
docs/work-items/ORCH-026/13-test-report.md
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-026
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-026
|
||||
|
||||
Задача: «Управление зависимостями задач (B ждёт A) в очереди» + сериализация merge/деплоя
|
||||
одного репо. Ветка `feature/ORCH-026-b-a`. Review-вердикт: **APPROVED** (`12-review.md`).
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3
|
||||
- Ветка: `feature/ORCH-026-b-a` (HEAD `aaa4829`)
|
||||
- Прод-оркестратор (8500): `/health` → `{"status":"ok"}` (не перезапускался, self-hosting инвариант соблюдён)
|
||||
- Дата: 2026-06-08
|
||||
|
||||
## Результаты по тест-плану (04-test-plan.yaml)
|
||||
|
||||
### Уровень A — сериализация merge/деплоя
|
||||
|
||||
| TC ID | Описание | Тест-функция | Результат |
|
||||
|-------|----------|--------------|-----------|
|
||||
| TC-A01 | Proactive pre-merge rebase (всегда, даже когда не behind) | `test_orch026_premerge_rebase::test_always_rebases_even_when_not_behind` | PASS |
|
||||
| TC-A02 | Расширенное окно merge-lease, defer не откат; holder-aware release | `test_orch026_merge_serialize::test_second_task_same_repo_defers_not_rollback`, `test_holder_aware_release_keeps_foreign_lease` | PASS |
|
||||
| TC-A03 | Сериализация строго per-repo (orchestrator ≠ enduro-trails) | `test_orch026_merge_serialize::test_serialization_is_strictly_per_repo` | PASS |
|
||||
| TC-A04 | Restart-safe + реклейм мёртвого держателя lease | `test_orch026_merge_serialize::test_dead_holder_lease_is_reclaimed`, `test_stale_lease_age_reclaimed_on_acquire` | PASS |
|
||||
| TC-A05 | Anti-livelock defer: bounded бюджет, эскалация | `test_orch026_merge_serialize::test_defer_budget_is_bounded` | PASS |
|
||||
| TC-A06 | Условность/kill-switch: off + out-of-scope = no-op | `test_orch026_conditionality::test_out_of_scope_repo_is_noop_even_with_flag_on`, `test_premerge_rebase::test_flag_off_short_circuits_like_orch043` | PASS |
|
||||
| TC-A07 | Self-hosting safety: только `--force-with-lease` на ветку, STAGE_TRANSITIONS не тронуты | `test_orch026_conditionality::test_premerge_only_force_with_lease_on_branch`, `test_stage_transitions_unchanged` | PASS |
|
||||
| TC-A08 | Сквозной сценарий сериализации merge-окна | `test_orch026_serialize_integration::test_serialized_merge_window` | PASS |
|
||||
|
||||
### Уровень B — декларативные зависимости
|
||||
|
||||
| TC ID | Описание | Тест-функция | Результат |
|
||||
|-------|----------|--------------|-----------|
|
||||
| TC-B01 | Декларация/резолв blocked-by; never-raise при недоступности | `test_orch026_task_deps::test_add_dependency_declares_and_resolves`, `test_add_dependency_never_raises_on_bad_input` | PASS |
|
||||
| TC-B02 | Гейт готовности: незавершённый depends-on → не ready; все done → ready | `test_orch026_task_deps::test_is_task_ready_blocked_then_ready`, `test_is_task_ready_no_deps_is_ready` | PASS |
|
||||
| TC-B03 | Детект циклов A→B→A и длиннее; ацикличный → нет | `test_orch026_dep_cycles::test_detect_two_node_cycle`, `test_detect_longer_cycle`, `test_acyclic_graph_has_no_cycle`, `test_detect_cycle_never_raises_on_garbage` | PASS |
|
||||
| TC-B04 | Цикл → Blocked + alert без падения воркера | `test_orch026_dep_cycles::test_handle_cycle_blocks_and_alerts`, `test_handle_cycle_never_raises_when_notify_fails` | PASS |
|
||||
| TC-B05 | claim_next_job не клеймит заблокированную (слот свободен), разблокируется при done | `test_orch026_task_deps::test_claim_skips_dep_blocked_job`, `test_claim_prefers_unblocked_job_over_blocked` | PASS |
|
||||
| TC-B06 | Видимость: строка ожидания в карточке; never-raise рендер | `test_orch026_dep_visibility::test_blocked_task_shows_waiting_line`, `test_render_never_raises_on_dep_error` | PASS |
|
||||
| TC-B07 | reconciler F-1 не разблокирует dep-заблокированную | `test_orch026_task_deps::test_reconciler_skip_helper_honours_block` | PASS |
|
||||
| TC-B08 | Сквозной: B стартует только после A→done; multiple predecessors | `test_orch026_deps_integration::test_b_waits_for_a_then_runs`, `test_multiple_predecessors_all_must_be_done`, `test_ingest_plane_relations_writes_db` | PASS |
|
||||
|
||||
### Общие / миграция / регресс
|
||||
|
||||
| TC ID | Описание | Тест-функция | Результат |
|
||||
|-------|----------|--------------|-----------|
|
||||
| TC-G01 | Аддитивная миграция job_deps: идемпотентна, данные сохранены | `test_orch026_migration::test_job_deps_table_created`, `test_job_deps_indices_created`, `test_migration_idempotent_and_preserves_data` | PASS |
|
||||
| TC-G02 | Наблюдаемость GET /queue: read-only блок task_deps | `test_orch026_queue_observability::test_queue_endpoint_includes_task_deps`, `test_snapshot_*` | PASS |
|
||||
| TC-G03 | Регресс: полный pytest зелёный | `tests/` (991 passed) | PASS |
|
||||
|
||||
## Smoke test API (прод 8500)
|
||||
- `GET /health` → `{"status":"ok","service":"orchestrator"}` — OK
|
||||
- `GET /status` → активные задачи отдаются, ORCH-026 (id 58) в стадии `testing` — OK
|
||||
- `GET /queue` → counts/resilience/reconcile/reaper/merge_verify читаются; брейкер `closed`, preflight OK — OK
|
||||
- Примечание: блок `task_deps` в `/queue` прода 8500 ОТСУТСТВУЕТ — ожидаемо: прод-контейнер несёт текущую задеплоенную версию, ORCH-026 ещё не выкатан (self-hosting, деплой на поздних стадиях). Фича наблюдаемости верифицирована in-branch тестом `test_queue_endpoint_includes_task_deps` (PASS) через TestClient на коде ветки.
|
||||
|
||||
## Вывод pytest
|
||||
```
|
||||
tests/test_orch026_*.py — 50 passed, 1 warning in 1.56s
|
||||
tests/ — 991 passed, 1 warning in 26.52s
|
||||
```
|
||||
(единственный warning — PydanticDeprecatedSince20 в `src/config.py`, предсуществующий, не относится к ORCH-026)
|
||||
|
||||
## Покрытие критериев приёмки (03-acceptance-criteria.md)
|
||||
Все 16 критериев (AC-A1…A7, AC-B1…B5, AC-G1…G5) покрыты прохождением соответствующих TC и
|
||||
подтверждены review-вердиктом APPROVED. Регрессии merge-gate (ORCH-043), merge-verify
|
||||
(ORCH-073), reconciler (ORCH-053/068), reaper (ORCH-065) не обнаружено.
|
||||
|
||||
## Итог
|
||||
**PASS** — 50/50 новых ORCH-026-тестов зелёные, полный регресс 991 passed, smoke API OK,
|
||||
прод-контейнер не затронут. Задача готова к переходу на `deploy-staging`.
|
||||
12
docs/work-items/ORCH-026/14-deploy-log.md
Normal file
12
docs/work-items/ORCH-026/14-deploy-log.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
deploy_status: SUCCESS
|
||||
work_item: ORCH-026
|
||||
hook_exit_code: 0
|
||||
deployed_by: deploy-finalizer
|
||||
---
|
||||
|
||||
# Deploy log — ORCH-036 executable self-deploy
|
||||
|
||||
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
|
||||
|
||||
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.
|
||||
34
docs/work-items/ORCH-026/15-staging-log.md
Normal file
34
docs/work-items/ORCH-026/15-staging-log.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-08T16:14:11+00:00
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed. Exit code 0 → advance.
|
||||
|
||||
Canonical run (ORCH-048, ADR-001) inside the live staging container:
|
||||
|
||||
```
|
||||
docker exec orchestrator-staging \
|
||||
python3 /repos/orchestrator/scripts/staging_check.py \
|
||||
--base-url http://localhost:8501 --mode stub
|
||||
```
|
||||
|
||||
## Result: 8/10 checks PASS
|
||||
|
||||
- **Block A (SMOKE):** A1 /health, A2 /queue, A3 ORCH_STAGING=true — all PASS.
|
||||
- **Block B (ACCESS):** B4 Plane sandbox (R), B5 Gitea orchestrator-sandbox (R+push), B6 registry isolation (sandbox present, prod ET/ORCH absent) — all PASS.
|
||||
- **Block C (E2E, stub):** C7 create issue, C8 trigger pipeline — PASS.
|
||||
|
||||
REAL failed: **none** — all pipeline checks green.
|
||||
|
||||
## INFRA-WAIVED (ORCH-061)
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
C9a/C9b are the two known sandbox-infra-only checks (depend on SANDBOX bot accounts being members of the sandbox Plane project, not on the pipeline). They are tolerated because every REAL check is green; the script printed `INFRA-WAIVED:` and exited 0 (fail-closed semantics preserved: any REAL failure would still yield exit 1).
|
||||
7
docs/work-items/ORCH-036/00-business-request.md
Normal file
7
docs/work-items/ORCH-036/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: ORCH-36: Исполняемый самодеплой — стадия deploy дёргает хост-хук (Вариант B)
|
||||
|
||||
Work Item ID: ORCH-036
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
109
docs/work-items/ORCH-036/01-brd.md
Normal file
109
docs/work-items/ORCH-036/01-brd.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# BRD — ORCH-36: Исполняемый самодеплой (стадия deploy дёргает хост-хук, Вариант B)
|
||||
|
||||
Work Item: ORCH-036
|
||||
Stage: analysis
|
||||
Автор: analyst
|
||||
Дата: 2026-06-06
|
||||
|
||||
## 1. Контекст и проблема
|
||||
|
||||
Стадия `deploy` конвейера сейчас **«бумажная»**. На ней deployer-агент (LLM) только
|
||||
пишет `docs/work-items/<wi>/14-deploy-log.md` с `deploy_status: SUCCESS|FAILED`, а QG
|
||||
`check_deploy_status` (`src/qg/checks.py:464`) парсит этот вердикт и пускает `deploy → done`.
|
||||
**Реального docker-деплоя нет** — продакшен орка катается руками (Стрим).
|
||||
|
||||
Хост-хук `scripts/orchestrator-deploy-hook.sh` **уже существует** (ORCH-34) и умеет:
|
||||
захват PREV_IMG → `git pull` → рестарт сервиса → health-check (10×6с = 60с) →
|
||||
авто-rollback при провале health, с корректным exit-code. Дефолты — STAGING-безопасные;
|
||||
прод включается через override env (`TARGET_SERVICE`, `TARGET_PORT`, `TARGET_IMAGE`,
|
||||
`COMPOSE_PROFILE`).
|
||||
|
||||
**Главная мина (self-hosting):** прод-контейнер `orchestrator` (8500) — ОДИН на все
|
||||
проекты, и в нём же бежит сам deployer-агент. Deployer не может синхронно рестартить
|
||||
контейнер, в котором живёт (`docker compose up -d orchestrator` убьёт его процесс на
|
||||
середине). Реальный рестарт self-репо обязан делать ВНЕШНИЙ хост-хук (вне контейнера),
|
||||
который срабатывает ПОСЛЕ выхода агента. Рубильник — снаружи; орк только ИНИЦИИРУЕТ.
|
||||
|
||||
## 2. Цель
|
||||
|
||||
Превратить стадию `deploy` в РЕАЛЬНЫЙ самодеплой: после зелёного `deploy-staging`-гейта
|
||||
конвейер вызывает хост-хук с прод-параметрами, хук промоутит образ в прод (8500) с
|
||||
health-чеком и авто-rollback. Результат хука (exit-code) маппится в `deploy_status`.
|
||||
**На старте — с ОБЯЗАТЕЛЬНЫМ ручным approve** (`DEPLOY_REQUIRE_MANUAL_APPROVE=true`):
|
||||
прод не трогается без явного «go» Владельца.
|
||||
|
||||
## 3. Ценность для бизнеса
|
||||
|
||||
- Уходит последний ручной шаг конвейера (прод-деплой Стрим) → шаг к автономному внедрению (эпик ORCH-54).
|
||||
- `deploy_status: SUCCESS` становится **доказанным** (реальный health-ok), а не декларацией LLM.
|
||||
- Гарантия build-once: «что протестировали на staging — то и в проде» (тот же образ, без пересборки).
|
||||
- Прод никогда не остаётся в нерабочем состоянии: авто-rollback + health-таймаут.
|
||||
|
||||
## 4. Заинтересованные стороны
|
||||
|
||||
| Роль | Интерес |
|
||||
|------|---------|
|
||||
| Владелец (Слава/Стрим) | Контроль через ручной approve; уведомления о каждом промоуте/откате |
|
||||
| Проект enduro-trails | Прод-орк не должен падать (общий инстанс) — групповой риск |
|
||||
| Конвейер ORCH | Стадия `deploy` исполняемая, гейты не сломаны |
|
||||
|
||||
## 5. Объём (scope)
|
||||
|
||||
### В объёме
|
||||
1. Исполнение реального прод-деплоя из стадии `deploy` через хост-хук (ssh / detached на хосте).
|
||||
2. Обязательный ручной approve-гейт ПОСЛЕ зелёного staging и ДО прод-рестарта (флаг включён).
|
||||
3. Маппинг exit-code хука → `deploy_status: SUCCESS|FAILED` (реальный, не бумажный).
|
||||
4. Уведомления (Plane-коммент + Telegram) на каждый промоут и откат.
|
||||
5. Build-once: перетегирование образа, прошедшего staging, без пересборки.
|
||||
6. Обновление `deployer.md` и `INFRA.md` (документация = golden source).
|
||||
7. Для НЕ-self репо (enduro-trails и др.) — деплой по ssh на их хост (поведение не ломается).
|
||||
|
||||
### Вне объёма (явно)
|
||||
- **Включение полного авто** (`DEPLOY_REQUIRE_MANUAL_APPROVE: true → false`) — отдельная задача,
|
||||
ТОЛЬКО после набора метрик доверия (см. §7). В этой задаче флаг НЕ выключается.
|
||||
- Изменение `docker-compose.yml` без явной необходимости.
|
||||
- Изменение стадий `STAGE_TRANSITIONS`, реестра QG, terminal-sync `deploy → done`.
|
||||
- Прод-деплой в реальный бой во время разработки задачи (отладка — только на staging-цели хука).
|
||||
|
||||
## 6. Бизнес-требования
|
||||
|
||||
- **BR-1.** После зелёного `deploy-staging`-гейта стадия `deploy` РЕАЛЬНО собирает/перетегирует
|
||||
образ, рестартит целевой сервис и проверяет health — не пишет бумажный SUCCESS.
|
||||
- **BR-2.** Для self-репо `orchestrator` рестарт 8500 выполняется ВНЕШНИМ (detached/host)
|
||||
процессом; deployer-агент НЕ убивает контейнер, в котором работает.
|
||||
- **BR-3.** `deploy_status: SUCCESS` пишется ТОЛЬКО при health-ok хука; провал/health-fail →
|
||||
`deploy_status: FAILED` → откат на `development` (как ORCH-35 staging-rollback, БАГ-8).
|
||||
- **BR-4.** Ручной approve обязателен (флаг `true`): без явного «go» прод НЕ трогается.
|
||||
- **BR-5.** Каждый промоут и откат уведомляет Владельца: Plane-коммент в задачу + Telegram.
|
||||
«Молчаливых» деплоев нет.
|
||||
- **BR-6.** Build-once: в прод идёт тот образ, что прошёл staging-гейт (перетег, не пересборка).
|
||||
- **BR-7.** Staging-гейт (`check_staging_status`) остаётся обязательным предусловием прод-деплоя.
|
||||
- **BR-8.** Прод никогда не остаётся в нерабочем состоянии — авто-rollback при провале health.
|
||||
- **BR-9.** Существующие гейты и инварианты не ломаются: `check_deploy_status`,
|
||||
`_parse_deploy_status`, rollback `deploy → development` (БАГ-8), terminal-sync `deploy → done`,
|
||||
merge-gate (ORCH-43).
|
||||
- **BR-10.** Документация (`deployer.md`, `INFRA.md`, `CHANGELOG.md`) обновлена в том же PR.
|
||||
|
||||
## 7. Критерии готовности к включению ПОЛНОГО авто (вне этой задачи)
|
||||
|
||||
Переключать `DEPLOY_REQUIRE_MANUAL_APPROVE: true → false` можно ТОЛЬКО когда закрыты ВСЕ 5:
|
||||
1. ≥10 успешных промоутов подряд (staging зелёный → approve → прод поднялся, откат не нужен).
|
||||
2. Zero false-negative: staging-гейт ни разу не пропустил битый деплой как «зелёный».
|
||||
3. Авто-rollback проверен в бою (≥2–3 реальных срабатывания), recovery 100%, MTTR < 60с.
|
||||
4. Ни одного «молчаливого» деплоя (каждый промоут/откат уведомил Владельца).
|
||||
5. Период наблюдения ≥10 деплоев ИЛИ ≥2 недели без инцидентов в режиме manual-approve.
|
||||
|
||||
## 8. Риски
|
||||
|
||||
| Риск | Влияние | Митигация |
|
||||
|------|---------|-----------|
|
||||
| Падение прод-орка 8500 при self-деплое | Встаёт конвейер ВСЕХ проектов | Detached host-хук + health + авто-rollback; отладка на staging-цели |
|
||||
| Deployer рестартит сам себя синхронно | Процесс агента убит на середине | BR-2: рестарт только внешним detached-процессом |
|
||||
| Преждевременный `deploy_status: SUCCESS` (хук ещё не закончил) | Задача уходит в done при незавершённом деплое | Гейт читает РЕАЛЬНЫЙ исход хука (механизм — на дизайне) |
|
||||
| Деплой без approve | Неконтролируемый прод-деплой | BR-4: approve-гейт блокирует до «go» |
|
||||
| Пересборка вместо перетега | В прод уезжает не то, что тестировали | BR-6: build-once, `--no-build` + retag |
|
||||
|
||||
## 9. Связанные задачи
|
||||
ORCH-7 (self-hosting), ORCH-21 (auto-rollback), ORCH-34 (хук готов), ORCH-35 (staging-гейт),
|
||||
ORCH-43 (merge-gate в проде), ORCH-54 (эпик автономного внедрения).
|
||||
Дизайн-референс: `tasks/orchestrator/DESIGN_STAGING_ENV.md §4/§7`.
|
||||
136
docs/work-items/ORCH-036/02-trz.md
Normal file
136
docs/work-items/ORCH-036/02-trz.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# ТЗ — ORCH-36: Исполняемый самодеплой (стадия deploy дёргает хост-хук, Вариант B)
|
||||
|
||||
Work Item: ORCH-036
|
||||
Stage: analysis
|
||||
Автор: analyst
|
||||
Дата: 2026-06-06
|
||||
|
||||
> Документ фиксирует ТРЕБОВАНИЯ к изменениям (что и где). Конкретный механизм
|
||||
> (ssh vs docker.sock vs detached nohup/systemd-run; механизм approve) выбирает
|
||||
> архитектор в ADR (`06-adr/`). ТЗ задаёт границы и контракты, не реализацию.
|
||||
|
||||
## 1. Текущее устройство (as-is, разведано в коде)
|
||||
|
||||
- **Стадии** (`src/stages.py`): `… testing → deploy-staging → deploy → done`.
|
||||
- `deploy-staging`: `agent=deployer`, `qg=check_staging_status` (запускается deployer при
|
||||
выходе из `deploy-staging`, входе в `deploy`).
|
||||
- `deploy`: `agent=None`, `qg=check_deploy_status` (агент НЕ запускается при выходе из `deploy`).
|
||||
- **Вывод:** реальную работу стадии `deploy` делает deployer-агент, запущенный на переходе
|
||||
`deploy-staging → deploy`. Он пишет `14-deploy-log.md`. Когда он завершается, `advance_stage`
|
||||
с `current_stage=deploy` прогоняет `check_deploy_status` и двигает `deploy → done`.
|
||||
- **QG** (`src/qg/checks.py`):
|
||||
- `check_deploy_status:464` → `_parse_deploy_status:406` читает ТОЛЬКО `deploy_status:` из
|
||||
YAML-frontmatter `14-deploy-log.md` (worktree → origin/main fallback → not found).
|
||||
- `check_staging_status:580` — условный (реален только для self-hosting `orchestrator`).
|
||||
- `is_self_hosting_repo()` (`:511`) — детектор self-репо.
|
||||
- **Откаты/диспетчеризация** (`src/stage_engine.py`):
|
||||
- `_handle_qg_failure_rollbacks:585` — ветка `deployer` + `check_deploy_status` FAILED →
|
||||
откат `deploy → development`, `set_issue_blocked`, release merge-lease, Plane+Telegram.
|
||||
- Terminal-sync `deploy → done` (`:281`) → `set_issue_done`, release merge-lease.
|
||||
- merge-gate (ORCH-43) на ребре `deploy-staging → deploy` — НЕ трогать.
|
||||
- **Launcher** (`src/agents/launcher.py`):
|
||||
- deployer-агент конфиг: `.task-deploy.md` / `.openclaw/agents/deployer.md` (`:180`).
|
||||
- Пост-обработка: commit+push артефактов в worktree (`:506-558`).
|
||||
- `exit_code != 0 && agent == deployer` → откат `deploy → development` (`:560-581`).
|
||||
- **Хост-хук** (`scripts/orchestrator-deploy-hook.sh`, ORCH-34) — ГОТОВ: `--deploy`/`--rollback`,
|
||||
параметризован env, дефолты STAGING; health 10×6с; авто-rollback; exit 0/1/2.
|
||||
- **Agent (deployer.md)**: на стадии `deploy` сейчас пишет «бумажный» вердикт; в промпте маркер
|
||||
«Real docker/SSH deploys are handled by scripts/orchestrator-deploy-hook.sh (ORCH-36)».
|
||||
- **Топология** (`docs/operations/INFRA.md`): prod=8500 (`.env`), staging=8501 (`.env.staging`,
|
||||
profile staging). Контейнер под uid 1000, доступ к docker.sock через gid 999.
|
||||
|
||||
## 2. Изменения по модулям (to-be)
|
||||
|
||||
### 2.1 `scripts/orchestrator-deploy-hook.sh` (донастройка прод-режима)
|
||||
- Хук уже параметризован; требуется обеспечить **корректный прод-профиль вызова**:
|
||||
`TARGET_SERVICE=orchestrator`, `TARGET_PORT=8500`, `TARGET_IMAGE=orchestrator-orchestrator`,
|
||||
`COMPOSE_PROFILE` (для прод-сервиса — пустой/дефолтный, т.к. prod стартует без profile).
|
||||
- **Build-once (BR-6):** деплой должен использовать образ, прошедший staging (перетег
|
||||
staging-образа → прод-тег + `docker compose up -d --no-build`), а НЕ пересобирать. Если
|
||||
текущий хук всегда `--no-build` и тянет `git pull` — уточнить в ADR, как гарантируется
|
||||
идентичность артефакта staging↔prod (retag staging image, либо общий build-once шаг).
|
||||
- `PREV_IMAGE_FILE` для прод — отдельный путь (например `.deploy-prev-image` без `-staging`),
|
||||
чтобы не путать снапшоты prod/staging.
|
||||
- Поведение `--rollback`, health-loop, exit-code (0=ok, 1=rolled back, 2=rollback тоже упал) —
|
||||
НЕ менять контракт.
|
||||
|
||||
### 2.2 Approve-гейт (новое; место — на дизайне)
|
||||
- Ввести флаг конфигурации `DEPLOY_REQUIRE_MANUAL_APPROVE` (bool, дефолт `true`).
|
||||
- При `true`: перед вызовом прод-хука (после зелёного `deploy-staging`) конвейер ОСТАНАВЛИВАЕТСЯ
|
||||
и ждёт явного «go» Владельца. Без «go» прод-хук НЕ вызывается.
|
||||
- Механизм approve (выбрать ОДИН в ADR): Plane-коммент-триггер (по образцу `:approved:`
|
||||
в `check_analysis_approved`) / Telegram-кнопка / signal-файл. Требование к механизму:
|
||||
рестарт-safe (переживает перезапуск инстанса), идемпотентный, аудируемый.
|
||||
- При `false` (вне этой задачи): approve-шаг пропускается — НЕ реализовывать выключение здесь,
|
||||
только заложить ветку по флагу.
|
||||
|
||||
### 2.3 Триггер реального деплоя из стадии `deploy`
|
||||
- На стадии `deploy` (для self-репо `orchestrator`) вместо/в дополнение к записи вердикта
|
||||
агентом — ИНИЦИИРОВАТЬ внешний detached-процесс (host-хук), который выполнит
|
||||
build-once+restart+health ПОСЛЕ выхода агента (BR-2: агент не рестартит сам себя).
|
||||
- Маршрут вызова (на дизайне): ssh на хост (`DEPLOY_SSH_USER`/`DEPLOY_HOOK_SCRIPT`) ИЛИ
|
||||
detached через docker.sock/nohup/systemd-run. Требование: процесс хука переживает выход
|
||||
агента и завершение его сессии.
|
||||
- Для **не-self** репо (enduro-trails): деплой по ssh на их хост (как раньше) — поведение не ломать.
|
||||
|
||||
### 2.4 Маппинг результата хука → `deploy_status`
|
||||
- `deploy_status: SUCCESS` пишется в `14-deploy-log.md` ТОЛЬКО при exit-code хука = 0 (health-ok).
|
||||
- exit-code ≠ 0 (1 = rolled back; 2 = rollback тоже упал) → `deploy_status: FAILED`.
|
||||
- **Контракт `_parse_deploy_status` НЕ меняется** (читает `deploy_status: SUCCESS|FAILED` из
|
||||
frontmatter). Меняется только КТО и КОГДА пишет этот вердикт — на основе реального исхода.
|
||||
- **Гонка чтения гейта:** т.к. self-рестарт асинхронный (detached), гейт `check_deploy_status`
|
||||
не должен прочитать вердикт ДО завершения хука. Механизм синхронизации (post-factum запись
|
||||
лога/мердж в main / отложенный гейт) — спроектировать в ADR так, чтобы гейт читал РЕАЛЬНЫЙ
|
||||
итог. Контракт чтения из worktree→origin/main (`_deploy_log_from_main`) можно переиспользовать.
|
||||
|
||||
### 2.5 Уведомления (BR-5)
|
||||
- На промоут (старт прод-деплоя + успех) и на откат → `plane_add_comment(work_item_id, …)` +
|
||||
`send_telegram(…)`. Переиспользовать существующие хелперы (`src/notifications.py`,
|
||||
`src/plane_sync.py`). Никаких «молчаливых» деплоев.
|
||||
|
||||
### 2.6 Конфигурация (`src/config.py` / `.env.example` / `.env.staging.example`)
|
||||
- Новый: `deploy_require_manual_approve: bool = True` (env `ORCH_DEPLOY_REQUIRE_MANUAL_APPROVE`).
|
||||
- Прод-параметры хука: `DEPLOY_SSH_USER`, `DEPLOY_SSH_HOST`, `DEPLOY_HOOK_SCRIPT` (уже есть в
|
||||
INFRA-карте) + прод-override `TARGET_SERVICE/PORT/IMAGE`. Прописать дескрипторы в `.env.example`
|
||||
(значения — только на хосте, не коммитить).
|
||||
- Условность по репо: реальный прод-деплой — только для self-hosting (`is_self_hosting_repo`),
|
||||
как ORCH-35; прочие репо идут прежним ssh-путём.
|
||||
|
||||
### 2.7 Документация (BR-10, golden source)
|
||||
- `.openclaw/agents/deployer.md` — раздел «Stage: deploy»: переписать с «бумажного SUCCESS» на
|
||||
«стадия ВЫЗЫВАЕТ хук»; зафиксировать запрет синхронного рестарта 8500 и detached-путь self.
|
||||
- `docs/operations/INFRA.md` — процедура прод-деплоя орка через хук + approve.
|
||||
- `docs/operations/DEPLOY_HOOK.md` — обновить, если затронут контракт хука.
|
||||
- `CHANGELOG.md` — запись о включении исполняемого деплоя (manual-approve).
|
||||
- ADR в `docs/work-items/ORCH-036/06-adr/ADR-NNN-*.md` (создаёт архитектор).
|
||||
|
||||
## 3. API
|
||||
- Изменений публичного HTTP API (`/health`, `/status`, `/queue`, `/webhook/*`) **не требуется**.
|
||||
- Если approve реализуется через Plane-коммент — переиспользуется существующий webhook-путь
|
||||
(`POST /webhook/plane`), новый endpoint не вводится. Если через signal-файл/Telegram —
|
||||
внешний по отношению к HTTP API механизм. Решение — ADR.
|
||||
|
||||
## 4. Схема БД
|
||||
- Изменения схемы **не требуются** для базового сценария (вердикт — в `14-deploy-log.md`;
|
||||
approve-состояние желательно хранить рестарт-safe — допустимо через jobs/task_content или
|
||||
signal-файл, без новой таблицы). Если архитектор сочтёт нужным поле статуса approve —
|
||||
обосновать в ADR; по умолчанию — без миграции.
|
||||
|
||||
## 5. Требования к Quality Gates
|
||||
- `check_deploy_status` и `_parse_deploy_status` — контракт чтения НЕ менять (frontmatter only).
|
||||
- Откат `deploy → development` при `deploy_status: FAILED` (`stage_engine` БАГ-8) — сохранить.
|
||||
- Terminal-sync `deploy → done` и release merge-lease — сохранить.
|
||||
- merge-gate (`check_branch_mergeable`) на ребре `deploy-staging → deploy` — не затрагивать.
|
||||
- `check_staging_status` остаётся обязательным предусловием (BR-7).
|
||||
|
||||
## 6. Артефакты pipeline
|
||||
- Создаётся/обновляется: `docs/work-items/ORCH-036/14-deploy-log.md` (с РЕАЛЬНЫМ `deploy_status`).
|
||||
- Обновляются по pipeline: `06-adr/ADR-NNN-*.md`, `12-review.md`, `13-test-report.md`,
|
||||
`15-staging-log.md` (последующими агентами).
|
||||
|
||||
## 7. Нефункциональные требования
|
||||
- **Безопасность self-deploy:** рестарт 8500 — только внешним рубильником; орк не может
|
||||
необратимо убить себя.
|
||||
- **Идемпотентность** хука и approve-механизма; **рестарт-safe** approve-состояние.
|
||||
- **MTTR < 60с** при авто-rollback (health-loop хука 10×6с уже укладывается).
|
||||
- **Отладка только на staging-цели** хука; реальный прод — лишь после approve.
|
||||
97
docs/work-items/ORCH-036/03-acceptance-criteria.md
Normal file
97
docs/work-items/ORCH-036/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Критерии приёмки — ORCH-36: Исполняемый самодеплой (Вариант B)
|
||||
|
||||
Work Item: ORCH-036
|
||||
Stage: analysis
|
||||
Автор: analyst
|
||||
Дата: 2026-06-06
|
||||
|
||||
Формат: каждый критерий — проверяемое условие PASS/FAIL. Отладка и проверки
|
||||
выполняются на **staging-цели хука** (8501); реальный прод (8500) — только после approve.
|
||||
|
||||
---
|
||||
|
||||
## AC-1. Стадия deploy исполняет реальный деплой (не бумажный)
|
||||
- **PASS:** на стадии `deploy` (после зелёного `deploy-staging`) вызывается хост-хук,
|
||||
который реально перетегирует образ, рестартит целевой сервис и выполняет health-check;
|
||||
`deploy_status` отражает РЕАЛЬНЫЙ исход хука.
|
||||
- **FAIL:** `deploy_status: SUCCESS` пишется без фактического рестарта/health (бумажный лог).
|
||||
- **Проверка:** прогон на staging-цели хука; в логе хука видны retag + `up -d` + health-loop;
|
||||
exit-code хука соответствует записанному `deploy_status`.
|
||||
|
||||
## AC-2. Self-репо: рестарт 8500 — внешним detached-процессом, агент себя не убивает
|
||||
- **PASS:** для `orchestrator` рестарт 8500 выполняет процесс ВНЕ контейнера агента; deployer-агент
|
||||
завершается штатно (exit 0), его процесс не убит рестартом контейнера.
|
||||
- **FAIL:** deployer синхронно делает `docker compose up -d orchestrator` из контейнера и/или
|
||||
агент падает/обрывается на середине из-за рестарта собственного контейнера.
|
||||
- **Проверка:** симуляция на staging-цели; убедиться, что detached-процесс переживает выход агента.
|
||||
|
||||
## AC-3. deploy_status маппится из exit-code хука
|
||||
- **PASS:** exit-code хука 0 → `deploy_status: SUCCESS`; exit-code ≠ 0 (1/2) → `deploy_status: FAILED`.
|
||||
- **FAIL:** любой иной маппинг (например SUCCESS при exit 1).
|
||||
- **Проверка:** unit-тест маппинга exit-code → вердикт; интеграционный прогон с искусственным
|
||||
кодом возврата хука.
|
||||
|
||||
## AC-4. Провал деплоя → откат на development
|
||||
- **PASS:** при `deploy_status: FAILED` задача откатывается `deploy → development`
|
||||
(`set_issue_blocked`, Plane+Telegram), как в существующей ветке БАГ-8.
|
||||
- **FAIL:** при FAILED задача уходит в `done` или зависает.
|
||||
- **Проверка:** существующий контракт `stage_engine._handle_qg_failure_rollbacks` для
|
||||
`deployer`+`check_deploy_status` сохранён и срабатывает.
|
||||
|
||||
## AC-5. Ручной approve обязателен и реально тормозит прод
|
||||
- **PASS:** при `DEPLOY_REQUIRE_MANUAL_APPROVE=true` прод-хук НЕ вызывается до явного «go»;
|
||||
после «go» — вызывается.
|
||||
- **FAIL:** прод-хук дёргается без approve.
|
||||
- **Проверка:** прогон без «go» — целевой сервис НЕ перезапущен (нет записи рестарта в логе хука,
|
||||
не сменился uptime/контейнер); прогон с «go» — рестарт состоялся.
|
||||
|
||||
## AC-6. Уведомления о каждом промоуте и откате
|
||||
- **PASS:** на старт/успех прод-деплоя и на откат приходят и Plane-коммент в задачу, и Telegram.
|
||||
- **FAIL:** хотя бы один промоут/откат прошёл «молчаливо».
|
||||
- **Проверка:** в Plane-задаче и в Telegram-чате присутствуют сообщения для каждого исхода.
|
||||
|
||||
## AC-7. Build-once: в прод идёт образ, прошедший staging
|
||||
- **PASS:** прод-деплой использует тот же образ, что прошёл staging-гейт (retag + `--no-build`),
|
||||
без пересборки.
|
||||
- **FAIL:** прод-деплой пересобирает образ заново (артефакт может отличаться от протестированного).
|
||||
- **Проверка:** sha/тег образа прод == образ, валидированный на staging; в логе нет `build`.
|
||||
|
||||
## AC-8. Staging-гейт остаётся обязательным предусловием
|
||||
- **PASS:** прод-деплой недостижим без зелёного `check_staging_status` (`staging_status: SUCCESS`).
|
||||
- **FAIL:** прод-хук можно вызвать при FAILED/отсутствующем staging-вердикте.
|
||||
- **Проверка:** при `staging_status: FAILED` задача откатывается на development, до `deploy` не доходит.
|
||||
|
||||
## AC-9. Авто-rollback восстанавливает прод (симуляция битого деплоя)
|
||||
- **PASS:** при симуляции битого деплоя на staging-цели health не проходит → хук авто-откатывает
|
||||
на предыдущий образ → сервис снова healthy; exit-code = 1 (rolled back); MTTR < 60с.
|
||||
- **FAIL:** сервис остаётся нерабочим после провала деплоя.
|
||||
- **Проверка:** искусственно сломать health, прогнать хук, убедиться в восстановлении и exit 1.
|
||||
|
||||
## AC-10. Существующие инварианты не сломаны
|
||||
- **PASS:** не изменены контракты `check_deploy_status` / `_parse_deploy_status`,
|
||||
`STAGE_TRANSITIONS`, terminal-sync `deploy → done`, merge-gate (ORCH-43), rollback БАГ-8.
|
||||
- **FAIL:** любой из перечисленных контрактов изменён/сломан.
|
||||
- **Проверка:** существующие тесты deploy/staging/merge-gate зелёные; регресс-прогон `pytest tests/`.
|
||||
|
||||
## AC-11. Условность по репо (не-self не ломается)
|
||||
- **PASS:** для не-self репо (enduro-trails) деплой идёт прежним ssh-путём; self-логика (detached,
|
||||
approve, 8500) применяется только для `orchestrator`.
|
||||
- **FAIL:** не-self репо затронуты self-специфичной логикой и ломаются.
|
||||
- **Проверка:** `is_self_hosting_repo` корректно разводит пути; тест на не-self репо.
|
||||
|
||||
## AC-12. Флаг полного авто НЕ выключен в этой задаче
|
||||
- **PASS:** `DEPLOY_REQUIRE_MANUAL_APPROVE` остаётся `true`; переключение в `false` не делается.
|
||||
- **FAIL:** флаг выставлен в `false` в рамках задачи.
|
||||
- **Проверка:** дефолт конфигурации = `true`; в коде/`.env.example` нет принудительного `false`.
|
||||
|
||||
## AC-13. Документация обновлена (golden source)
|
||||
- **PASS:** обновлены `deployer.md` (стадия deploy = вызов хука), `INFRA.md` (процедура),
|
||||
`CHANGELOG.md`; заведён ADR в `06-adr/`.
|
||||
- **FAIL:** функционал изменён, документация — нет (Reviewer обязан вернуть REQUEST_CHANGES).
|
||||
- **Проверка:** диффы документации присутствуют в том же PR.
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done
|
||||
Все AC-1…AC-13 в статусе PASS; `pytest tests/` зелёный; артефакты pipeline на месте;
|
||||
прод (8500) во время разработки НЕ тронут (вся проверка — на staging-цели хука).
|
||||
122
docs/work-items/ORCH-036/04-test-plan.yaml
Normal file
122
docs/work-items/ORCH-036/04-test-plan.yaml
Normal file
@@ -0,0 +1,122 @@
|
||||
work_item: ORCH-036
|
||||
title: "Исполняемый самодеплой — стадия deploy дёргает хост-хук (Вариант B)"
|
||||
stage: analysis
|
||||
notes: >
|
||||
Все тесты — на изолированном уровне (unit/integration с моками subprocess/ssh
|
||||
и хука). Реальный прод (8500) НЕ трогается. Интеграционные прогоны хука — на
|
||||
staging-цели. Хост-хук (bash) проверяется отдельным интеграционным сценарием с
|
||||
поддельным health/exit-code; в pytest вызов хука мокается.
|
||||
|
||||
tests:
|
||||
# --- exit-code -> deploy_status mapping (AC-1, AC-3) ---
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "Маппинг exit-code хука 0 -> deploy_status: SUCCESS"
|
||||
module: tests/test_deploy_hook_mapping.py
|
||||
expected: PASS
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: "Маппинг exit-code хука 1 (rolled back) -> deploy_status: FAILED"
|
||||
module: tests/test_deploy_hook_mapping.py
|
||||
expected: PASS
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: "Маппинг exit-code хука 2 (rollback тоже упал) -> deploy_status: FAILED"
|
||||
module: tests/test_deploy_hook_mapping.py
|
||||
expected: PASS
|
||||
|
||||
# --- approve gate (AC-5, AC-12) ---
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: "DEPLOY_REQUIRE_MANUAL_APPROVE дефолт == true в settings"
|
||||
module: tests/test_deploy_approve.py
|
||||
expected: PASS
|
||||
- id: TC-05
|
||||
type: integration
|
||||
description: "Флаг true и нет 'go' -> прод-хук НЕ вызывается (subprocess/ssh не дёрнут)"
|
||||
module: tests/test_deploy_approve.py
|
||||
expected: PASS
|
||||
- id: TC-06
|
||||
type: integration
|
||||
description: "Флаг true и есть 'go' -> прод-хук вызывается ровно один раз"
|
||||
module: tests/test_deploy_approve.py
|
||||
expected: PASS
|
||||
|
||||
# --- self vs non-self routing (AC-2, AC-11) ---
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: "is_self_hosting_repo('orchestrator') == True; иной репо -> False (не регрессировал)"
|
||||
module: tests/test_deploy_routing.py
|
||||
expected: PASS
|
||||
- id: TC-08
|
||||
type: integration
|
||||
description: "self-репо orchestrator: рестарт инициируется detached/host-процессом, не синхронно из агента"
|
||||
module: tests/test_deploy_routing.py
|
||||
expected: PASS
|
||||
- id: TC-09
|
||||
type: integration
|
||||
description: "не-self репо (enduro-trails): деплой идёт прежним ssh-путём, self-логика не применяется"
|
||||
module: tests/test_deploy_routing.py
|
||||
expected: PASS
|
||||
|
||||
# --- rollback on FAILED (AC-4) ---
|
||||
- id: TC-10
|
||||
type: integration
|
||||
description: "deploy_status: FAILED -> откат deploy->development, set_issue_blocked, release merge-lease"
|
||||
module: tests/test_deploy_rollback.py
|
||||
expected: PASS
|
||||
|
||||
# --- staging precondition preserved (AC-8) ---
|
||||
- id: TC-11
|
||||
type: integration
|
||||
description: "staging_status: FAILED -> до стадии deploy не доходит (откат на development)"
|
||||
module: tests/test_staging_precondition.py
|
||||
expected: PASS
|
||||
|
||||
# --- notifications (AC-6) ---
|
||||
- id: TC-12
|
||||
type: integration
|
||||
description: "Успешный промоут -> и Plane-коммент, и Telegram отправлены"
|
||||
module: tests/test_deploy_notifications.py
|
||||
expected: PASS
|
||||
- id: TC-13
|
||||
type: integration
|
||||
description: "Откат -> и Plane-коммент, и Telegram отправлены (нет молчаливого деплоя)"
|
||||
module: tests/test_deploy_notifications.py
|
||||
expected: PASS
|
||||
|
||||
# --- build-once (AC-7) ---
|
||||
- id: TC-14
|
||||
type: integration
|
||||
description: "Прод-деплой использует образ staging (retag, без build) — нет шага docker build"
|
||||
module: tests/test_deploy_build_once.py
|
||||
expected: PASS
|
||||
|
||||
# --- regression: unchanged gate contracts (AC-10) ---
|
||||
- id: TC-15
|
||||
type: unit
|
||||
description: "_parse_deploy_status: SUCCESS->(True), FAILED->(False), нет frontmatter->(False) — контракт цел"
|
||||
module: tests/test_qg_checks.py
|
||||
expected: PASS
|
||||
- id: TC-16
|
||||
type: unit
|
||||
description: "STAGE_TRANSITIONS deploy->done и agent/qg deploy не изменены"
|
||||
module: tests/test_stages.py
|
||||
expected: PASS
|
||||
- id: TC-17
|
||||
type: integration
|
||||
description: "terminal-sync deploy->done (set_issue_done + release merge-lease) сохранён"
|
||||
module: tests/test_deploy_terminal_sync.py
|
||||
expected: PASS
|
||||
- id: TC-18
|
||||
type: integration
|
||||
description: "merge-gate на ребре deploy-staging->deploy не затронут (регресс ORCH-43 зелёный)"
|
||||
module: tests/test_merge_gate.py
|
||||
expected: PASS
|
||||
|
||||
# --- auto-rollback hook behavior (AC-9) ---
|
||||
- id: TC-19
|
||||
type: integration
|
||||
description: "Симуляция битого деплоя на staging-цели: health fail -> авто-rollback -> healthy, exit 1, MTTR<60с"
|
||||
module: tests/test_deploy_hook_rollback_sim.py
|
||||
expected: PASS
|
||||
@@ -0,0 +1,184 @@
|
||||
# ADR-001: Исполняемый самодеплой — стадия `deploy` дёргает хост-хук (Вариант B)
|
||||
|
||||
Work Item: ORCH-036
|
||||
Stage: architecture
|
||||
Автор: architect
|
||||
Дата: 2026-06-06
|
||||
|
||||
## Статус
|
||||
Accepted
|
||||
|
||||
## Контекст
|
||||
|
||||
Стадия `deploy` сейчас «бумажная»: deployer-агент (LLM) пишет в `14-deploy-log.md`
|
||||
`deploy_status: SUCCESS|FAILED`, а гейт `check_deploy_status` (`src/qg/checks.py:464`)
|
||||
парсит этот вердикт и двигает `deploy → done`. Реального docker-деплоя нет (прод
|
||||
катается руками). BRD ORCH-36 требует превратить стадию в РЕАЛЬНЫЙ самодеплой с
|
||||
обязательным ручным approve, build-once и авто-rollback (BR-1…BR-10).
|
||||
|
||||
Три твёрдых ограничения, разведанных в коде, определяют дизайн:
|
||||
|
||||
1. **Self-restart (BR-2).** Прод-контейнер `orchestrator` (8500) — ОДИН на все
|
||||
проекты, и в нём же исполняется deployer. `docker compose up -d orchestrator`
|
||||
из контейнера убьёт процесс агента/воркера на середине. Реальный рестарт обязан
|
||||
делать ВНЕШНИЙ процесс на хосте, переживающий гибель контейнера.
|
||||
2. **Status-only verdict model.** Комментарии Plane НЕ управляют конвейером —
|
||||
механизм `:approved:`/`:rejected:` был удалён (`src/webhooks/plane.py:544`,
|
||||
bug-3 «echo self-hit»). Единственный человеческий гейт — **смена статуса Plane
|
||||
на `Approved`** (`handle_verdict` → `_try_advance_stage` → `advance_stage`).
|
||||
3. **Гонка чтения гейта.** Так как реальный рестарт асинхронный и убивает контейнер,
|
||||
`check_deploy_status` нельзя выполнять на выходе агента — вердикта ещё нет; его
|
||||
преждевременное чтение → ложный FAILED → ложный откат.
|
||||
|
||||
Контракты, которые НЕ меняются (BR-9, AC-10): `STAGE_TRANSITIONS`,
|
||||
`check_deploy_status` / `_parse_deploy_status` (frontmatter only), откат БАГ-8
|
||||
(`deploy → development`), terminal-sync `deploy → done`, merge-gate (ORCH-43),
|
||||
exit-code-контракт хука (0/1/2).
|
||||
|
||||
## Решение
|
||||
|
||||
Деплой стадии `deploy` для self-hosting (`orchestrator`) разбивается на **три фазы**,
|
||||
оркеструемые детерминированным кодом (без LLM в критическом пути self-restart). Для
|
||||
НЕ-self репо (enduro-trails и пр.) поведение НЕ меняется — прежний синхронный
|
||||
ssh-деплой агентом.
|
||||
|
||||
### Условность по репо
|
||||
Вся новая логика гейтится `is_self_hosting_repo(repo)` (как ORCH-35). Не-self репо
|
||||
идут существующим путём: deployer-агент на стадии `deploy` делает ssh-деплой
|
||||
синхронно, пишет `14-deploy-log.md`, гейт срабатывает на выходе агента.
|
||||
|
||||
### Фаза A — запрос approve (вход в `deploy`)
|
||||
В `advance_stage` на ребре `deploy-staging → deploy` (ПОСЛЕ зелёного
|
||||
`check_staging_status` и merge-gate ORCH-43), для self-hosting + `deploy_require_
|
||||
manual_approve=true`:
|
||||
- **НЕ** ставить в очередь прод-deployer (перехватить штатный
|
||||
`enqueue_job(get_agent_for_stage("deploy-staging"))`);
|
||||
- выставить issue в approval-pending статус (паттерн `set_issue_in_review`),
|
||||
написать Plane-коммент «approve для прод-деплоя» + Telegram (BR-5);
|
||||
- записать restart-safe маркер `approve-requested` (sentinel-файл, см. ниже).
|
||||
|
||||
Задача остаётся на стадии `deploy` и ждёт человека. `STAGE_TRANSITIONS` не меняется.
|
||||
|
||||
При `deploy_require_manual_approve=false` (вне объёма, флаг НЕ выключается в ORCH-36 —
|
||||
AC-12) Фаза A сразу переходит к Фазе B без человеческого гейта. Структурная ветка
|
||||
закладывается, но дефолт `true`.
|
||||
|
||||
### Фаза B — инициация деплоя (смена статуса Plane → Approved)
|
||||
Человек ставит issue в `Approved`. `handle_verdict(approved=True)` →
|
||||
`_try_advance_stage` → `advance_stage(current_stage="deploy", finished_agent=None)`.
|
||||
Новая ветка-перехват в `advance_stage`:
|
||||
- условие: `current_stage=="deploy"` И `finished_agent is None` (человеческий путь)
|
||||
И self-hosting И approve-флаг И маркер `initiated` ОТСУТСТВУЕТ;
|
||||
- действие: запустить **внешний detached host-процесс** (см. ниже) и поставить в
|
||||
очередь детерминированный **finalizer-job** с задержкой; записать маркер
|
||||
`initiated` (идемпотентность: повторный Approved не запускает деплой дважды);
|
||||
Plane-коммент «прод-деплой стартовал» + Telegram (BR-5);
|
||||
- **вернуться БЕЗ advance** (НЕ запускать `check_deploy_status` — вердикта ещё нет).
|
||||
|
||||
Дискриминатор `finished_agent` разводит Фазу B (человек, `None`) и Фазу C
|
||||
(finalizer, `"deployer"`), поэтому повторное использование `advance_stage` безопасно.
|
||||
|
||||
### Фаза C — фиксация вердикта (детерминированный finalizer)
|
||||
Finalizer-job (claim'ится воркером уже в НОВОМ контейнере после рестарта):
|
||||
- читает sentinel `result` (exit-code хука, записан host-процессом);
|
||||
- если `result` ещё нет и бюджет попыток не исчерпан → **defer** (повторный
|
||||
finalizer-job с `available_at_delay_s`, как merge-gate defer); бюджет считается
|
||||
из `jobs` (`LIKE '%deploy-finalize%'`, restart-safe);
|
||||
- если `result` есть → **маппинг exit-code → deploy_status** (детерминированный,
|
||||
unit-тестируемый): `0 → SUCCESS`, `1|2|иное → FAILED`; записать
|
||||
`14-deploy-log.md` (frontmatter `deploy_status:`), смержить в `main` (паттерн
|
||||
лога), затем вызвать `advance_stage(current_stage="deploy", finished_agent="deployer")`;
|
||||
- далее срабатывают СУЩЕСТВУЮЩИЕ контракты: `SUCCESS` → terminal-sync `deploy → done`
|
||||
+ release merge-lease; `FAILED` → откат БАГ-8 `deploy → development` +
|
||||
`set_issue_blocked` + Plane/Telegram (BR-3, AC-4). `_parse_deploy_status` НЕ меняется.
|
||||
|
||||
### Механизм detached-запуска: ssh + setsid
|
||||
Выбор: **ssh на хост (`slin@DEPLOY_SSH_HOST`) с setsid-detached исполнением** хука.
|
||||
Обоснование: ssh-ключи уже смонтированы (INFRA P-2), не-self репо уже деплоятся по
|
||||
ssh (единый путь), хук живёт на хосте и под `slin` имеет полный доступ к docker вне
|
||||
контейнера → переживает рестарт 8500 (BR-2). `setsid`/`nohup` + redirect отвязывает
|
||||
удалённый процесс от ssh-канала, чтобы он пережил гибель ssh-клиента при рестарте
|
||||
контейнера. Отвергнуто: вызов через docker.sock изнутри контейнера = ровно мина
|
||||
«убей себя на середине вызова».
|
||||
|
||||
Эскиз (точная сборка — за разработчиком):
|
||||
```
|
||||
ssh -o StrictHostKeyChecking=no slin@$DEPLOY_SSH_HOST \
|
||||
"setsid bash -c 'cd /home/slin/repos/orchestrator && \
|
||||
SOURCE_IMAGE=orchestrator-orchestrator-staging \
|
||||
TARGET_SERVICE=orchestrator TARGET_PORT=8500 \
|
||||
TARGET_IMAGE=orchestrator-orchestrator COMPOSE_PROFILE= \
|
||||
PREV_IMAGE_FILE=.deploy-prev-image-prod \
|
||||
bash scripts/orchestrator-deploy-hook.sh --deploy; \
|
||||
echo \$? > <result-sentinel>' >> <hook.log> 2>&1 </dev/null &"
|
||||
```
|
||||
ssh-команда возвращается сразу; remote-процесс detached. Запись sentinel `result`
|
||||
делает **обёртка** (`echo $? > result`), а НЕ хук — контракт хука нетронут.
|
||||
|
||||
### Build-once (BR-6, AC-7)
|
||||
Прод обязан подняться на ОБРАЗЕ, прошедшем staging (а не на пересборке). Решение:
|
||||
расширить хук **опциональным** `SOURCE_IMAGE` (обратно совместимо: не задан →
|
||||
текущее поведение). При заданном `SOURCE_IMAGE` хук ПЕРЕД `up -d --no-build`
|
||||
делает `docker tag $SOURCE_IMAGE $TARGET_IMAGE`. Для прод-self:
|
||||
`SOURCE_IMAGE=orchestrator-orchestrator-staging` → `TARGET_IMAGE=orchestrator-orchestrator`.
|
||||
Это единственное допустимое изменение хука; exit-code-контракт и дефолтное
|
||||
staging-поведение не меняются. `git pull` хука обновляет рабочее дерево хоста для
|
||||
будущих сборок, но РАЗВЁРНУТЫЙ артефакт = перетегированный staging-образ.
|
||||
|
||||
### Restart-safe состояние: sentinel-файлы (без миграции БД)
|
||||
По образцу merge-lease (`<repos_dir>/.merge-lease-<repo>.json`) состояние деплоя
|
||||
хранится в файлах под `<repos_dir>/.deploy-state-<repo>/<work_item_id>/` (вне git,
|
||||
видны и хосту, и контейнеру через mount `/home/slin/repos ↔ /repos`):
|
||||
- `approve-requested` — Фаза A выполнена;
|
||||
- `initiated` — Фаза B запущена (idempotency-guard);
|
||||
- `result` — exit-code хука (пишет host-обёртка).
|
||||
Бюджет finalize-defer считается из `jobs` (restart-safe), новых таблиц/колонок НЕТ
|
||||
(TRZ §4).
|
||||
|
||||
## Последствия
|
||||
|
||||
### Плюсы
|
||||
- `deploy_status: SUCCESS` становится ДОКАЗАННЫМ (реальный health-ok хука), не
|
||||
декларацией LLM (BR-1).
|
||||
- Self-restart безопасен: рестарт 8500 делает внешний host-процесс; орк себя не
|
||||
убивает (BR-2). Вердикт фиксирует НОВЫЙ контейнер после рестарта.
|
||||
- Критический путь self-restart **детерминирован** (без LLM) — главный выигрыш по
|
||||
безопасности self-hosting; зеркалит детерминизм merge-gate ORCH-43.
|
||||
- Approve вписан в существующую status-only модель — restart-safe, аудируемо в Plane,
|
||||
идемпотентно (маркер `initiated`).
|
||||
- Гонка чтения гейта закрыта: гейт читает РЕАЛЬНЫЙ итог через finalizer-defer.
|
||||
- Build-once гарантирует «что тестировали — то в проде».
|
||||
- Нетронуты: `STAGE_TRANSITIONS`, реестр QG, `_parse_deploy_status`, БАГ-8,
|
||||
terminal-sync, merge-gate, контракт хука (exit-code).
|
||||
|
||||
### Минусы / ограничения
|
||||
- Вводится **новый детерминированный job-handler** в очереди (reserved-agent
|
||||
`deploy-finalizer`, не-LLM) — расширение dispatch воркера/лаунчера. Контейнированное,
|
||||
но это новая под-компонента → задача помечается `arch:major-change`.
|
||||
- Перехваты в `advance_stage` усложняют стадию `deploy` (три ветки по
|
||||
`finished_agent`/маркерам). Требуется аккуратное покрытие тестами (TC-04…TC-09).
|
||||
- Build-once зависит от того, что deploy-staging оставил валидный образ
|
||||
`orchestrator-orchestrator-staging`; при rebase merge-gate возможен дрейф
|
||||
образ↔main (см. 10-tech-risks R-3).
|
||||
- Approve = смена статуса Plane на `Approved`; человек должен понимать, что на
|
||||
стадии `deploy` `Approved` означает «деплой в прод» (документируется в deployer.md
|
||||
и INFRA.md).
|
||||
|
||||
### Что обязан сделать developer
|
||||
1. `src/config.py`: `deploy_require_manual_approve: bool = True` + прод-параметры
|
||||
хука/ssh + `deploy_finalize_delay_s` / `deploy_finalize_max_attempts`.
|
||||
2. `src/stage_engine.py`: перехваты Фазы A/B + ветка finalizer (Фаза C через
|
||||
`advance_stage(..., finished_agent="deployer")`).
|
||||
3. Очередь: reserved-agent `deploy-finalizer` (детерминированный handler:
|
||||
read-result | defer | map+write+advance). Маппинг exit→status — отдельная
|
||||
чистая функция (unit TC-01/02/03).
|
||||
4. `scripts/orchestrator-deploy-hook.sh`: опциональный `SOURCE_IMAGE` retag
|
||||
(обратно совместимо) + прод `PREV_IMAGE_FILE`.
|
||||
5. Уведомления (Plane+Telegram) на initiate/success/rollback (BR-5).
|
||||
6. Документация: `deployer.md`, `INFRA.md`, `DEPLOY_HOOK.md`, `CHANGELOG.md`.
|
||||
7. Отладка — только на staging-цели хука; прод 8500 в разработке не трогать.
|
||||
|
||||
## Связанные решения
|
||||
- Глобальный ADR: `docs/architecture/adr/adr-0007-executable-self-deploy.md`.
|
||||
- ORCH-35 staging-gate (`adr-0003`), ORCH-43 merge-gate (`adr-0006`),
|
||||
ORCH-21 auto-rollback, ORCH-34 хук, ORCH-40 run-as-host-uid (`adr-0005`).
|
||||
48
docs/work-items/ORCH-036/07-infra-requirements.md
Normal file
48
docs/work-items/ORCH-036/07-infra-requirements.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Инфраструктурные требования — ORCH-036
|
||||
|
||||
Work Item: ORCH-036
|
||||
Stage: architecture
|
||||
Автор: architect
|
||||
|
||||
> Топология не меняется (та же mva154, те же два контейнера). Меняется ПРОЦЕДУРА
|
||||
> прод-деплоя орка: из ручной → исполняемая через хост-хук с ручным approve.
|
||||
|
||||
## 1. Контейнеры / порты — без изменений
|
||||
- prod `orchestrator` (8500), staging `orchestrator-staging` (8501) — как в INFRA.md.
|
||||
- Образы (имена для build-once): prod `orchestrator-orchestrator`,
|
||||
staging `orchestrator-orchestrator-staging`.
|
||||
|
||||
## 2. Хост-предусловия (Owner, в git не коммитятся)
|
||||
- **HP-1.** ssh-доступ из контейнера на хост: `ssh slin@$DEPLOY_SSH_HOST` работает
|
||||
под uid 1000 ключом из `~/.orchestrator-ssh` (INFRA P-2). Без него detached-запуск
|
||||
Фазы B невозможен.
|
||||
- **HP-2.** `<repos_dir>/.deploy-state-<repo>/` доступен на запись и хосту (host-обёртка
|
||||
пишет `result`), и контейнеру (finalizer читает) — обеспечивается mount
|
||||
`/home/slin/repos ↔ /repos` (как merge-lease).
|
||||
- **HP-3.** `PREV_IMAGE_FILE` для прод — отдельный путь
|
||||
(`.deploy-prev-image-prod`), чтобы не путать снапшоты prod/staging.
|
||||
- **HP-4 (P-4 из INFRA).** Прод-рестарт self — только в окно тишины; общий инстанс
|
||||
с enduro-trails. На старте — под ручным approve (флаг `true`).
|
||||
|
||||
## 3. Переменные окружения (карта; значения — на хосте, в git только дескрипторы)
|
||||
| Переменная | Назначение | Дефолт |
|
||||
|-----------|-----------|--------|
|
||||
| `ORCH_DEPLOY_REQUIRE_MANUAL_APPROVE` | ручной approve перед прод-деплоем | `true` |
|
||||
| `DEPLOY_SSH_USER` / `DEPLOY_SSH_HOST` | ssh-цель хост-хука | — (INFRA-карта) |
|
||||
| `DEPLOY_HOOK_SCRIPT` | путь к хуку на хосте | `scripts/orchestrator-deploy-hook.sh` |
|
||||
| прод `TARGET_SERVICE/PORT/IMAGE`, `COMPOSE_PROFILE` | override прод-профиля хука | `orchestrator`/`8500`/`orchestrator-orchestrator`/пусто |
|
||||
| `SOURCE_IMAGE` (новый параметр хука) | образ для build-once retag | пусто → текущее поведение |
|
||||
| `ORCH_DEPLOY_FINALIZE_DELAY_S` | задержка перед первым finalize-поллом | > 60с (health-loop хука) |
|
||||
| `ORCH_DEPLOY_FINALIZE_MAX_ATTEMPTS` | бюджет finalize-defer | bounded (anti-livelock) |
|
||||
|
||||
Прописать дескрипторы в `.env.example` / INFRA.md. Реальные значения не коммитить.
|
||||
|
||||
## 4. Сетевые / процессные требования
|
||||
- Detached host-процесс (ssh + setsid) обязан пережить рестарт прод-контейнера 8500.
|
||||
- Finalizer-job исполняется в НОВОМ контейнере после рестарта (очередь restart-safe).
|
||||
- MTTR авто-rollback < 60с (health-loop хука 10×6с уже укладывается, BR-8/AC-9).
|
||||
|
||||
## 5. Что НЕ требуется
|
||||
- Новых контейнеров/портов/сервисов — нет.
|
||||
- Изменений `docker-compose.yml` — не требуется (build-once через retag, не профиль).
|
||||
- Multi-node / облако / message-queue — нет (принципы проекта).
|
||||
34
docs/work-items/ORCH-036/08-data-requirements.md
Normal file
34
docs/work-items/ORCH-036/08-data-requirements.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Требования к данным / схеме БД — ORCH-036
|
||||
|
||||
Work Item: ORCH-036
|
||||
Stage: architecture
|
||||
Автор: architect
|
||||
|
||||
## Решение: миграция БД НЕ требуется
|
||||
|
||||
Схема SQLite (`events`, `tasks`, `agent_runs`, `jobs`) не меняется. Обоснование:
|
||||
|
||||
1. **Вердикт деплоя** — в `14-deploy-log.md` (frontmatter `deploy_status:`), как
|
||||
сейчас. `_parse_deploy_status` не трогаем (AC-10).
|
||||
2. **Approve / initiated / result-состояние** — restart-safe через **sentinel-файлы**
|
||||
под `<repos_dir>/.deploy-state-<repo>/<work_item_id>/` (паттерн merge-lease
|
||||
`<repos_dir>/.merge-lease-<repo>.json`), а не через новую таблицу/колонку:
|
||||
- `approve-requested` — Фаза A;
|
||||
- `initiated` — Фаза B (idempotency-guard);
|
||||
- `result` — exit-code хука (пишет host-обёртка).
|
||||
3. **Бюджет finalize-defer** считается из существующей таблицы `jobs`
|
||||
(`task_content LIKE '%deploy-finalize%'`), как `_merge_defer_count` для merge-gate
|
||||
— restart-safe, без новых полей.
|
||||
4. **Finalizer-job** использует существующую структуру `jobs` (agent, repo,
|
||||
task_content, task_id, available_at). Reserved-agent `deploy-finalizer` — это
|
||||
значение в колонке `agent`, схема не меняется.
|
||||
|
||||
## Почему файлы, а не БД
|
||||
- Sentinel должен быть виден И хосту (пишет `result`), И контейнеру (читает finalizer);
|
||||
файл на общем mount это обеспечивает, SQLite-запись из host-обёртки — нет.
|
||||
- Зеркалит уже принятый паттерн merge-lease (ORCH-43) — единообразие, restart-safe,
|
||||
crash-реклейм по возрасту файла.
|
||||
|
||||
Если разработчик при реализации сочтёт необходимым поле статуса approve в БД —
|
||||
это требует обновления данного ADR с обоснованием; по умолчанию — без миграции
|
||||
(согласовано с TRZ §4).
|
||||
23
docs/work-items/ORCH-036/10-tech-risks.md
Normal file
23
docs/work-items/ORCH-036/10-tech-risks.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Технические риски — ORCH-036
|
||||
|
||||
Work Item: ORCH-036
|
||||
Stage: architecture
|
||||
Автор: architect
|
||||
|
||||
| ID | Риск | Влияние | Вероятность | Митигация |
|
||||
|----|------|---------|-------------|-----------|
|
||||
| R-1 | Detached host-процесс не пережил рестарт 8500 (ssh-канал убит вместе с контейнером) | Деплой не завершён, `result` не записан, finalizer вечно defer'ит | Средняя | `setsid`/`nohup` + redirect отвязывает remote-процесс от ssh; интеграционная проверка на staging-цели (TC-08); finalize-defer bounded → по исчерпании `set_issue_blocked` + Telegram |
|
||||
| R-2 | Преждевременное чтение `check_deploy_status` (вердикта ещё нет) | Ложный FAILED → ложный откат на development | Средняя | Фаза B возвращается БЕЗ advance; гейт запускает только finalizer (Фаза C) после появления `result`; defer пока `result` отсутствует |
|
||||
| R-3 | Дрейф образ↔main: merge-gate сделал rebase, но staging-образ собран до rebase → build-once тегирует «не тот» код | В прод уезжает не точно то, что в `main` | Низкая | merge-gate (ORCH-43) делает re-test после rebase; build-once = «что валидировано на staging», что и есть контракт; задокументировано как осознанное ограничение; усиление (rebuild+revalidate staging после rebase) — отдельная задача |
|
||||
| R-4 | Двойной Approved (человек кликнул дважды / дубль webhook) запускает деплой дважды | Двойной рестарт прода, гонка | Средняя | Маркер `initiated` (idempotency-guard); event-dedup webhook'ов Plane уже есть |
|
||||
| R-5 | exit 2 хука (rollback тоже упал) → 8500 лежит → finalizer/новый контейнер не поднялся | Конвейер всех проектов встал | Низкая | health-loop + авто-rollback хука минимизируют; `restart: unless-stopped` поднимет контейнер на ПРЕДЫДУЩЕМ образе если retag не случился; exit 2 → `deploy_status: FAILED` + откат + Telegram-алерт; ручной `--rollback` хука как backstop |
|
||||
| R-6 | Reserved-agent `deploy-finalizer` ошибочно уйдёт в LLM-путь лаунчера (`_spawn` → ValueError) | Finalizer не отработает | Низкая | Перехват ДО `_spawn` в `launch_job`; unit-тест маршрутизации |
|
||||
| R-7 | sentinel-файлы не видны контейнеру/хосту (mount/uid) | Фазы B/C не синхронизируются | Низкая | Тот же mount и uid-модель, что у merge-lease (ORCH-40/43); HP-2 в 07-infra |
|
||||
| R-8 | Approve через смену статуса Plane конфликтует с auto-advance других стадий | Случайный `Approved` на `deploy` ничего не ломает, но семантика неочевидна | Низкая | Перехват по `current_stage=="deploy"` + `finished_agent is None` + маркеры; задокументировать в deployer.md/INFRA, что `Approved` на `deploy` = «деплой в прод» |
|
||||
| R-9 | Самодеплой ORCH ломает прод во время разработки самой ORCH-36 | Групповой простой (enduro-trails) | Низкая | Вся отладка — на staging-цели хука (8501); прод 8500 не трогать (AC: DoD); флаг approve=true |
|
||||
|
||||
## Сводный приоритет
|
||||
- **Блокеры дизайна:** R-1, R-2 — закрыты архитектурой (setsid-detached + finalizer-defer).
|
||||
- **Безопасность self-hosting:** R-5, R-9 — закрыты обязательным approve + staging-отладкой
|
||||
+ авто-rollback + `restart: unless-stopped`.
|
||||
- **Корректность:** R-3, R-4 — осознанные ограничения / idempotency-guard.
|
||||
64
docs/work-items/ORCH-036/12-review.md
Normal file
64
docs/work-items/ORCH-036/12-review.md
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-036
|
||||
verdict: APPROVED
|
||||
version: 2
|
||||
---
|
||||
|
||||
# Review ORCH-036 — Исполняемый самодеплой стадии `deploy` (Вариант B)
|
||||
|
||||
## Summary
|
||||
|
||||
Re-review после фикса двух P1 из версии 1. Оба блокера устранены:
|
||||
|
||||
1. **Stale deploy-state маркеры** — добавлен `self_deploy.clear_state(repo, work_item_id)`
|
||||
(never-raise, idempotent, рекурсивное удаление `<repos_dir>/.deploy-state-<repo>/<wi>/`)
|
||||
в ветке БАГ-8-отката `check_deploy_status` FAILED (`_handle_qg_failure_rollbacks`,
|
||||
`src/stage_engine.py`) и дополнительно в начале Фазы A (`_handle_self_deploy_phase_a`)
|
||||
как belt-and-suspenders. Добавлен регрессионный тест
|
||||
`tests/test_deploy_rollback.py::test_tc11_re_deploy_after_rollback_not_wedged`,
|
||||
доказывающий, что после FAILED → откат → фикс → повторный заход на `deploy` Фаза B
|
||||
РЕАЛЬНО инициирует деплой (нет no-op по устаревшему `initiated`), плюс
|
||||
`tests/test_deploy_hook_mapping.py::test_clear_state_removes_all_markers_and_is_idempotent`.
|
||||
2. **`.env.example`** — добавлен полный блок дескрипторов `ORCH_SELF_DEPLOY_*` /
|
||||
`ORCH_DEPLOY_*` (14 настроек, плейсхолдеры, секреты не коммитятся) по образцу
|
||||
merge-gate ORCH-043, с подробными комментариями.
|
||||
|
||||
Реализация трёхфазного исполняемого самодеплоя соответствует ADR-001 и закрывает
|
||||
критерии приёмки AC-1…AC-13. Контракты `STAGE_TRANSITIONS` / `QG_CHECKS` /
|
||||
`_parse_deploy_status` / БАГ-8 / terminal-sync / merge-gate (ORCH-43) НЕ тронуты;
|
||||
условность по репо (`self_deploy_applies`) корректна; перехваты упорядочены верно
|
||||
(Phase B после terminal-check, Phase A после merge-gate); `deploy-finalizer` —
|
||||
детерминированный no-LLM reserved-agent, перехвачен в launcher до `_spawn`. Все
|
||||
импорты (`set_issue_in_review`, `plane_add_comment`, `set_issue_blocked`,
|
||||
`send_telegram`) присутствуют. `pytest tests/` — **568 passed**.
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- (нет)
|
||||
|
||||
### P1 — Must fix
|
||||
- (нет — оба P1 из версии 1 устранены и покрыты тестами)
|
||||
|
||||
### P2 — Should fix
|
||||
- (нет блокирующих; прежний P2 про сквозную процедуру оператора частично закрыт:
|
||||
env-карта новых настроек добавлена в INFRA.md, пошаговый approve→deploy описан в
|
||||
deployer.md и DEPLOY_HOOK.md)
|
||||
|
||||
## Документация
|
||||
|
||||
Обновлена содержательно и в том же PR:
|
||||
- `.openclaw/agents/deployer.md` — стадия `deploy` переписана: self-hosting путь
|
||||
(Фазы A/B/C, явный запрет рестарта 8500 изнутри агента) vs прежний синхронный
|
||||
ssh-путь для не-self репо;
|
||||
- `docs/operations/INFRA.md` — env-карта всех новых `ORCH_SELF_DEPLOY_*` / `ORCH_DEPLOY_*`;
|
||||
- `docs/operations/DEPLOY_HOOK.md` — `SOURCE_IMAGE` build-once + прод-пример;
|
||||
- `docs/architecture/README.md` — раздел «Исполняемый самодеплой стадии `deploy`»;
|
||||
- `CHANGELOG.md` — запись Added (фича) + запись Fixed (review-fix: clear_state + .env.example);
|
||||
- ADR `docs/work-items/ORCH-036/06-adr/ADR-001-executable-self-deploy.md` + глобальный
|
||||
`docs/architecture/adr/adr-0007-executable-self-deploy.md`;
|
||||
- **`.env.example`** — канонический шаблон (CLAUDE.md №8, ТЗ §2.6) дополнен (был пробел в v1).
|
||||
|
||||
Документация = golden source: изменения `src/` сопровождены синхронным обновлением
|
||||
доки в том же PR. Ось документации — PASS.
|
||||
90
docs/work-items/ORCH-036/13-test-report.md
Normal file
90
docs/work-items/ORCH-036/13-test-report.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-036
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-036
|
||||
|
||||
Исполняемый самодеплой стадии `deploy` (Вариант B) — дёргает хост-хук
|
||||
`scripts/orchestrator-deploy-hook.sh`, три фазы (A/B/C), условность по self-hosting репо.
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3 (pluggy 1.6.0, anyio 4.13.0, asyncio 0.23.8 — mode AUTO)
|
||||
- Worktree: `feature/ORCH-036-orch-36-deploy-b`
|
||||
- Дата: 2026-06-06
|
||||
- Prod (8500) во время тестов НЕ тронут: вся проверка изолированная (моки subprocess/ssh/хука).
|
||||
Smoke выполнялся read-only GET-запросами.
|
||||
|
||||
## Smoke test API (prod 8500, read-only)
|
||||
| Endpoint | Результат |
|
||||
|----------|-----------|
|
||||
| GET /health | `{"status":"ok","service":"orchestrator"}` — OK |
|
||||
| GET /status | OK (отдаёт активные задачи) |
|
||||
| GET /queue | OK (counts/max_concurrency/resilience; breaker=closed, preflight_ok=true) |
|
||||
|
||||
`curl` в окружении отсутствует — smoke выполнен через `urllib.request` (эквивалент GET).
|
||||
|
||||
## Результаты по тест-плану (04-test-plan.yaml)
|
||||
|
||||
| TC ID | Описание | Тест | Результат |
|
||||
|-------|----------|------|-----------|
|
||||
| TC-01 | exit 0 → deploy_status: SUCCESS | test_tc01_exit0_maps_to_success | PASS |
|
||||
| TC-02 | exit 1 (rolled back) → FAILED | test_tc02_exit1_rolled_back_maps_to_failed | PASS |
|
||||
| TC-03 | exit 2 (rollback тоже упал) → FAILED | test_tc03_exit2_rollback_also_failed_maps_to_failed | PASS |
|
||||
| TC-04 | DEPLOY_REQUIRE_MANUAL_APPROVE дефолт == true | test_tc04_manual_approve_default_true | PASS |
|
||||
| TC-05 | true и нет approve → прод-хук НЕ вызван | test_tc05_no_approve_does_not_call_prod_hook | PASS |
|
||||
| TC-06 | true и approve → прод-хук вызван ровно 1 раз | test_tc06_approved_calls_prod_hook_exactly_once | PASS |
|
||||
| TC-07 | is_self_hosting_repo: только orchestrator True | test_tc07_is_self_hosting_repo_only_orchestrator | PASS |
|
||||
| TC-08 | self-репо: рестарт detached host-процессом | test_tc08_self_repo_launches_detached_host_process | PASS |
|
||||
| TC-09 | не-self репо: прежний ssh-путь | test_tc09_non_self_repo_uses_legacy_path | PASS |
|
||||
| TC-10 | FAILED → откат deploy→development, blocked, release lease | test_tc10_failed_deploy_rolls_back_to_development | PASS |
|
||||
| TC-11 | staging_status FAILED → до deploy не доходит | test_tc11_staging_failed_never_reaches_deploy | PASS |
|
||||
| TC-12 | успех → Plane-коммент + Telegram | test_tc12_success_notifies_plane_and_telegram | PASS |
|
||||
| TC-13 | откат → Plane-коммент + Telegram | test_tc13_rollback_notifies_plane_and_telegram | PASS |
|
||||
| TC-14 | build-once: retag staging-образа, без build | test_tc14_deploy_command_retags_staging_image_no_build | PASS |
|
||||
| TC-15 | _parse_deploy_status контракт цел (проза не проходит) | test_qg_checks::test_tc15_* (5 кейсов) | PASS |
|
||||
| TC-16 | STAGE_TRANSITIONS deploy/deploy-staging не изменены | test_stages::test_tc16_* | PASS |
|
||||
| TC-17 | terminal-sync deploy→done сохранён | test_tc17_success_deploy_syncs_terminal_done | PASS |
|
||||
| TC-18 | merge-gate (ORCH-43) на ребре не затронут | test_merge_gate (14 кейсов) | PASS |
|
||||
| TC-19 | симуляция битого деплоя: авто-rollback → healthy, exit 1 | test_tc19_unhealthy_deploy_auto_rolls_back_exit1 | PASS |
|
||||
|
||||
Доп. регрессионные тесты (review-fix): `test_clear_state_removes_all_markers_and_is_idempotent`,
|
||||
`test_tc11_re_deploy_after_rollback_not_wedged` — оба PASS (stale deploy-state очищается, повторный
|
||||
заход на deploy после отката не зависает).
|
||||
|
||||
## Покрытие критериев приёмки
|
||||
|
||||
| AC | Покрыт тестами | Статус |
|
||||
|----|----------------|--------|
|
||||
| AC-1 реальный деплой (не бумажный) | TC-01..03, TC-14, TC-19 | PASS |
|
||||
| AC-2 self-репо рестарт detached, агент себя не убивает | TC-08 | PASS |
|
||||
| AC-3 deploy_status из exit-code | TC-01..03 | PASS |
|
||||
| AC-4 FAILED → откат на development | TC-10 | PASS |
|
||||
| AC-5 ручной approve реально тормозит прод | TC-05, TC-06 | PASS |
|
||||
| AC-6 уведомления о промоуте и откате | TC-12, TC-13 | PASS |
|
||||
| AC-7 build-once (образ из staging) | TC-14 | PASS |
|
||||
| AC-8 staging-гейт обязателен | TC-11 | PASS |
|
||||
| AC-9 авто-rollback восстанавливает прод (MTTR<60с) | TC-19 | PASS |
|
||||
| AC-10 инварианты не сломаны | TC-15..18 + полный регресс | PASS |
|
||||
| AC-11 условность по репо (не-self не ломается) | TC-07, TC-09 | PASS |
|
||||
| AC-12 флаг авто НЕ выключен (остаётся true) | TC-04 | PASS |
|
||||
| AC-13 документация обновлена | проверено reviewer (12-review.md, APPROVED) | PASS |
|
||||
|
||||
## Вывод pytest
|
||||
|
||||
Полный регресс:
|
||||
```
|
||||
======================= 568 passed, 1 warning in 15.25s ========================
|
||||
```
|
||||
(единственный warning — PydanticDeprecatedSince20 в `src/config.py`, не связан с задачей)
|
||||
|
||||
Целевые модули тест-плана:
|
||||
```
|
||||
======================== 46 passed, 1 warning in 2.17s =========================
|
||||
```
|
||||
|
||||
## Итог
|
||||
**PASS** — все 19 TC зелёные, все критерии приёмки AC-1…AC-13 покрыты, полный регресс
|
||||
568/568 passed, smoke API OK, прод (8500) не тронут. Задача готова к стадии deploy-staging.
|
||||
39
docs/work-items/ORCH-036/15-staging-log.md
Normal file
39
docs/work-items/ORCH-036/15-staging-log.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-06T21:47:48Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed against the live `orchestrator-staging` instance (port 8501).
|
||||
Executed canonically inside the container (ORCH-048, ADR-001):
|
||||
|
||||
```
|
||||
docker exec orchestrator-staging \
|
||||
python3 /repos/orchestrator/scripts/staging_check.py \
|
||||
--base-url http://localhost:8501 --mode stub
|
||||
```
|
||||
|
||||
(The agent container has no `docker` CLI; the canonical `docker exec` was invoked via the
|
||||
Docker Engine API over the mounted `/var/run/docker.sock`, which is equivalent — the command
|
||||
ran inside `orchestrator-staging` so the B6 registry-isolation check read the staging
|
||||
process-env `.env.staging`.)
|
||||
|
||||
**Result: 10/10 checks PASS — exit code 0.**
|
||||
|
||||
| Block | Check | Verdict |
|
||||
|-------|-------|---------|
|
||||
| A SMOKE | A1 `GET /health` → 200 status=ok | PASS |
|
||||
| A SMOKE | A2 `GET /queue` → 200 (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` | PASS |
|
||||
| C E2E | C9b Analyst job enqueued in staging queue | PASS |
|
||||
|
||||
CLEANUP: test branch deleted, Plane SANDBOX issue deleted, staging DB job/task rows removed
|
||||
(`try/finally` guaranteed). No prod (8500) container was touched.
|
||||
7
docs/work-items/ORCH-040/00-business-request.md
Normal file
7
docs/work-items/ORCH-040/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: Агенты пишут файлы под root в смонтированный хост-репо: ломает git/ребилд
|
||||
|
||||
Work Item ID: ORCH-040
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
106
docs/work-items/ORCH-040/01-brd.md
Normal file
106
docs/work-items/ORCH-040/01-brd.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# 01 — BRD: Агенты пишут файлы под root в смонтированный хост-репо
|
||||
|
||||
Work Item: **ORCH-040**
|
||||
Тип: инфра-фикс (runtime / docker-compose)
|
||||
Исполнение: через Dev напрямую (по решению Owner)
|
||||
|
||||
## 1. Бизнес-контекст и проблема
|
||||
|
||||
Контейнер `orchestrator` (prod, 8500) работает под `uid=0 (root)`. Он монтирует
|
||||
хостовый каталог `/home/slin/repos` → `/repos` (rw). Claude-CLI агенты запускаются
|
||||
через `subprocess.Popen` **внутри контейнера**, то есть тоже под root. Они пишут:
|
||||
|
||||
- в git worktree задач — `/repos/_wt/<repo>/<branch>/...`;
|
||||
- в прод-клон — `/repos/<repo>/docs/work-items/...` (через коммит/пуш из worktree).
|
||||
|
||||
В результате на **хосте** файлы создаются с владельцем `root:root`.
|
||||
|
||||
### Симптом
|
||||
При ребилде/деплое прода `git pull` / `git reset` под пользователем `slin` падает:
|
||||
|
||||
```
|
||||
error: insufficient permission for adding an object to repository database .git/objects
|
||||
Permission denied (на docs/work-items/ORCH-016, владелец root:root)
|
||||
```
|
||||
|
||||
Каждый будущий деплой будет ломаться, пока вручную не выполнить `chown`.
|
||||
|
||||
### Диагноз (живая разведка 05–06.06)
|
||||
- `docker exec orchestrator id` → `uid=0(root) gid=0(root) groups=0,999`.
|
||||
- Хост `slin` = `uid=1000 gid=1000`, группы: `sudo`, `docker(999)`.
|
||||
- `/home/slin/repos` → `/repos` (rw); на хосте `/repos` уже `1000:1000 rwxrwxr-x`.
|
||||
- `docs/work-items/*` на хосте — `root:root` (наследие прошлых прогонов).
|
||||
|
||||
## 2. Цель
|
||||
|
||||
Агенты конвейера **не должны** создавать `root`-файлы в хостовом репозитории.
|
||||
После любого прогона конвейера `git pull/status/reset` под `slin` на хосте
|
||||
работает **без ручного chown**.
|
||||
|
||||
## 3. Объём (scope)
|
||||
|
||||
В объёме:
|
||||
- Изменение runtime-режима контейнера так, чтобы артефакты создавались под
|
||||
`uid:gid` хоста (`1000:1000`).
|
||||
- Сохранение работоспособности: claude-auth (preflight), git/ssh, docker.sock
|
||||
(деплой), запуск конвейера.
|
||||
- Обновление документации (INFRA.md, CHANGELOG, ADR с обоснованием варианта).
|
||||
- Проверка на staging (8501) ДО прода.
|
||||
|
||||
Вне объёма:
|
||||
- Массовое исправление прав уже существующих `root:root` файлов в истории
|
||||
(разовый `chown` на хосте делает Owner; в задаче — только описать команду).
|
||||
- Изменение логики конвейера, QG, схемы БД.
|
||||
- Смена модели/effort агентов, прочие фичи.
|
||||
|
||||
## 4. Заинтересованные стороны
|
||||
- Owner (Слава) — заказчик, владелец хоста mva154.
|
||||
- Стрим — разведка/контекст.
|
||||
- Проект enduro-trails — co-tenant того же прод-инстанса (групповой риск).
|
||||
|
||||
## 5. Ограничения и риски (off-limits)
|
||||
|
||||
Self-hosting: прод-инстанс `orchestrator` ОДИН на все прод-проекты, общая БД и
|
||||
очередь. **Нельзя ломать**: запуск конвейера, доступ к Plane/Gitea/SSH из агентов,
|
||||
docker.sock. Любой рестарт контейнера под новым uid — **только в окно тишины**
|
||||
(нет активных задач). Тестировать на staging ПЕРЕД продом.
|
||||
|
||||
### Известные мины (подтверждены разведкой)
|
||||
- **МИНА 1 — docker.sock**: `/var/run/docker.sock` = `srw-rw---- root:999`.
|
||||
Доступ идёт через gid 999, не через root. При переходе на непривилегированный
|
||||
uid обязателен supplementary group `999`. *В текущем `docker-compose.yml` уже
|
||||
есть `group_add: ["999"]` для обоих сервисов — учесть, не сломать.*
|
||||
- **МИНА 2 — claude creds (БЛОКЕР)**: `/home/slin/.claude/.credentials.json` =
|
||||
`root:root 0600`. Сейчас читает контейнер-root. Под `uid=1000` без доступа →
|
||||
`claude-auth` ломается → весь конвейер умирает (preflight ORCH-044 заворачивает).
|
||||
Проверить ПЕРВЫМ.
|
||||
- **МИНА 3 — claude бинарь**: реальный бинарь `/opt/claude-code/bin/claude.exe`
|
||||
(root:root, `+x` для всех — ok). `ORCH_CLAUDE_BIN=/usr/bin/claude` в env не
|
||||
существует; launcher использует hardcode `CLAUDE_BIN=/opt/claude-code/bin/claude.exe`.
|
||||
Под uid 1000 исполним, но проверить запуск.
|
||||
- **SSH-маунт**: `/home/slin/.orchestrator-ssh` → `/root/.ssh:ro`. При смене uid
|
||||
HOME/домашний каталог меняется — путь к ключам нужно поправить (деплой по ssh).
|
||||
- **HOME**: launcher форсит `HOME=/home/slin` (две точки: env Popen и git_env).
|
||||
Креды читаются из `/home/slin/.claude`. Учесть при смене uid.
|
||||
|
||||
## 6. Бизнес-ценность
|
||||
Устранение постоянного ручного `chown` после каждого деплоя; деплой прода
|
||||
перестаёт ломаться на правах; снимается источник простоя конвейера всех проектов.
|
||||
|
||||
## 7. Допущения
|
||||
- Хост-каталоги `/app/data` и `/repos` уже `1000:1000` (запись под uid 1000 пройдёт).
|
||||
- Dockerfile уже содержит `git config --system --add safe.directory '*'`.
|
||||
- Окно тишины для рестарта контейнера согласуется с Owner.
|
||||
|
||||
## 8. Host-prerequisites (предусловия на стороне Owner)
|
||||
Часть фикса невозможно закрыть только кодом — есть действия на хосте mva154,
|
||||
которые выполняет Owner (в гит не коммитятся, фиксируются в ADR/INFRA). Это
|
||||
обязательные предусловия Варианта 1; без них переход на uid 1000 ломает конвейер:
|
||||
- **P-1 (блокер, МИНА 2):** обеспечить чтение `/home/slin/.claude/.credentials.json`
|
||||
под uid 1000 (рекомендация — `chown -R 1000:1000 /home/slin/.claude`). Способ
|
||||
выбирает ADR; анализ фиксирует факт предусловия.
|
||||
- **P-2:** ssh-ключи (`/home/slin/.orchestrator-ssh`) читаемы uid 1000.
|
||||
- **P-3:** подтверждение `slin = uid 1000 gid 1000` (подтверждено разведкой).
|
||||
- **P-4:** рестарт прод-self только в окно тишины (`GET /status` без активных задач).
|
||||
|
||||
Детализация и команды — в `02-trz.md` §10.
|
||||
112
docs/work-items/ORCH-040/02-trz.md
Normal file
112
docs/work-items/ORCH-040/02-trz.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# 02 — ТЗ: agent-файлы под uid хоста (не root)
|
||||
|
||||
Work Item: **ORCH-040**
|
||||
|
||||
## 1. Суть требования
|
||||
Артефакты конвейера (worktree + docs) должны создаваться на хосте под
|
||||
`uid:gid = 1000:1000` (slin), а не `root:root`. При этом сохраняется работа
|
||||
claude-auth, git, ssh-деплоя и docker.sock.
|
||||
|
||||
## 2. Задействованные модули и файлы
|
||||
|
||||
| Файл | Роль в задаче |
|
||||
|------|----------------|
|
||||
| `docker-compose.yml` | runtime-режим контейнера (prod `orchestrator` + `orchestrator-staging`). Основная точка изменения. |
|
||||
| `Dockerfile` | возможные правки под непривилегированный запуск (safe.directory уже есть; при необходимости — создание пользователя/прав). |
|
||||
| `src/agents/launcher.py` | `HOME=/home/slin` хардкод (env Popen ~стр.326 и git_env ~стр.513); путь `CLAUDE_BIN` (стр.187). Проверить совместимость при смене uid; править ТОЛЬКО при необходимости. |
|
||||
| `docs/operations/INFRA.md` | блок «Тома (volumes)» (SSH-маунт `/root/.ssh`), карта рантайма — обновить. |
|
||||
| `CHANGELOG.md` | запись об изменении. |
|
||||
| `docs/work-items/ORCH-040/06-adr/` | ADR с выбором варианта + обоснованием (создаёт архитектор). |
|
||||
|
||||
## 3. Варианты решения (вход для ADR — выбор и обоснование за архитектором)
|
||||
|
||||
> Анализ фиксирует варианты как требование «выбрать и обосновать в ADR».
|
||||
> Рекомендация разведки — Вариант 1.
|
||||
|
||||
1. **Вариант 1 (рекомендован): `user: "1000:1000"` в docker-compose.**
|
||||
Все файлы сразу `slin:slin`, git на хосте без chown. Обязательные довески:
|
||||
- сохранить/проверить `group_add: ["999"]` (docker.sock) — **уже присутствует**;
|
||||
- обеспечить доступ uid 1000 к claude creds (`/home/slin/.claude/.credentials.json`):
|
||||
`chown 1000:1000` на хосте ИЛИ права на чтение для 1000 (задокументировать);
|
||||
- поправить SSH-маунт: `/home/slin/.orchestrator-ssh` → домашний каталог uid 1000
|
||||
(`/home/slin/.ssh`), а не `/root/.ssh`; согласовать с `HOME` в launcher;
|
||||
- проверить запуск `claude.exe` + `git` + `ssh` под uid 1000.
|
||||
|
||||
2. **Вариант 2: subprocess агента под непривилегированным uid внутри контейнера**
|
||||
(`Popen preexec_fn setuid` / `gosu`). Точечно, но сложнее; контейнер остаётся root.
|
||||
|
||||
3. **Вариант 3 (fallback, костыль): chown-хук нормализации прав после стадии**
|
||||
(`chown -R 1000:1000` worktree/доки). Лечит симптом, не корень. Применять, только
|
||||
если В1 неустранимо рвёт creds/sock.
|
||||
|
||||
## 4. Требуемые изменения (при выбранном Варианте 1)
|
||||
|
||||
### 4.1 docker-compose.yml (оба сервиса: `orchestrator`, `orchestrator-staging`)
|
||||
- Добавить `user: "1000:1000"`.
|
||||
- Сохранить `group_add: ["999"]` (НЕ удалять).
|
||||
- Изменить SSH-маунт: target `/root/.ssh` → каталог `.ssh` пользователя 1000,
|
||||
синхронно с `HOME`, который форсит launcher (`/home/slin`). То есть привести к
|
||||
единому HOME: маунт `/home/slin/.orchestrator-ssh` → `/home/slin/.ssh:ro`.
|
||||
- Маунт `/home/slin/.claude` и `.claude.json` — оставить; проверить доступ uid 1000.
|
||||
|
||||
### 4.2 Доступ к claude creds
|
||||
- Обеспечить, что `/home/slin/.claude/.credentials.json` читается uid 1000
|
||||
(на хосте — операция Owner; в ТЗ зафиксировать команду и проверку).
|
||||
|
||||
### 4.3 src/agents/launcher.py
|
||||
- Проверить, что `HOME=/home/slin` остаётся валиден под uid 1000 (домашний каталог
|
||||
существует и доступен). Менять ТОЛЬКО при доказанной необходимости.
|
||||
- Не менять CLAUDE_BIN, если запуск под 1000 подтверждён.
|
||||
|
||||
### 4.4 Dockerfile
|
||||
- Менять при необходимости (например, гарантировать существование `/home/slin` и
|
||||
права). `git config --system --add safe.directory '*'` уже есть — оставить.
|
||||
|
||||
## 5. Изменения API
|
||||
Нет.
|
||||
|
||||
## 6. Изменения схемы БД
|
||||
Нет.
|
||||
|
||||
## 7. Новые QG checks
|
||||
Нет. Существующий staging-гейт (`check_staging_status`, ORCH-35) — обязательная
|
||||
страховка перед прод-деплоем self (без изменений).
|
||||
|
||||
## 8. Артефакты pipeline, которые должны быть созданы/обновлены
|
||||
- `06-adr/ADR-NNN-<slug>.md` — выбор варианта + обоснование (мины 1–3, SSH, HOME).
|
||||
- `docs/operations/INFRA.md` — обновить блок volumes (SSH target) и, при изменении
|
||||
режима, упоминание uid рантайма.
|
||||
- `CHANGELOG.md` — запись `fix:`/`refactor:` по Conventional Commits.
|
||||
- `12-review.md`, `13-test-report.md`, `15-staging-log.md` — по ходу конвейера.
|
||||
|
||||
## 9. Порядок безопасного внедрения (требование)
|
||||
1. Живая разведка прав creds/sock/ssh ДО кода.
|
||||
2. Применить и проверить на **staging (8501)** end-to-end.
|
||||
3. Прод-рестарт контейнера под новым uid — только в окно тишины (нет активных задач).
|
||||
4. Регресс на хосте: новые tracked-артефакты `1000:1000`, `git pull` под slin без ошибок.
|
||||
|
||||
## 10. Зависимости и host-prerequisites (действия на хосте, вне кода)
|
||||
|
||||
Эти пункты — предусловия для Варианта 1; их выполняет Owner на хосте mva154 (в гит
|
||||
не коммитятся, но фиксируются в ADR/INFRA как обязательная процедура). Без них
|
||||
переход контейнера на uid 1000 ломает конвейер (МИНА 2 — блокер).
|
||||
|
||||
| # | Предусловие | Команда / проверка | Зачем |
|
||||
|---|-------------|--------------------|-------|
|
||||
| P-1 | Доступ uid 1000 к claude creds | `chown -R 1000:1000 /home/slin/.claude` (вкл. `.credentials.json`); проверка `sudo -u '#1000' test -r /home/slin/.claude/.credentials.json` | МИНА 2: без доступа preflight ORCH-044 завернёт весь конвейер |
|
||||
| P-2 | SSH-ключи в HOME нового uid и читаемы | ключи в `/home/slin/.orchestrator-ssh` читаемы uid 1000; маунт ведёт в `/home/slin/.ssh` (см. §4.1) | деплой по ssh (DEPLOY_SSH_*) |
|
||||
| P-3 | Подтверждение uid:gid рантайма | `id slin` → `uid=1000 gid=1000`; `/repos` и `/app/data` уже `1000:1000` (подтверждено разведкой) | целевые файлы создаются под slin |
|
||||
| P-4 | Окно тишины для рестарта self | `GET /status` → нет активных задач перед рестартом прод-контейнера | self-hosting: общий инстанс с enduro-trails |
|
||||
|
||||
> **Открытый выбор для ADR (не решается анализом):** способ обеспечения P-1 —
|
||||
> `chown` creds (рекомендация разведки) vs. ослабление read-прав vs. отказ от
|
||||
> Варианта 1 в пользу Варианта 3 (chown-хук). Анализ фиксирует P-1 как
|
||||
> обязательное предусловие при любом из вариантов 1/2; для Варианта 3 — неактуально.
|
||||
|
||||
## 11. Подтверждённые факты текущего рантайма (anchor для Dev)
|
||||
Сверено с веткой `feature/ORCH-040-root-git` на 06.06:
|
||||
- `docker-compose.yml`: оба сервиса имеют `group_add: ["999"]` (МИНА 1 — НЕ удалять);
|
||||
SSH-маунт обоих = `/home/slin/.orchestrator-ssh:/root/.ssh:ro` (требует правки target);
|
||||
claude-маунты = `/home/slin/.claude` и `/home/slin/.claude.json:ro`.
|
||||
- `src/agents/launcher.py`: `HOME="/home/slin"` форсится в env Popen (стр. 326) и в
|
||||
git_env (стр. 513); `CLAUDE_BIN="/opt/claude-code/bin/claude.exe"` (стр. 187).
|
||||
62
docs/work-items/ORCH-040/03-acceptance-criteria.md
Normal file
62
docs/work-items/ORCH-040/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# 03 — Критерии приёмки: ORCH-040
|
||||
|
||||
Work Item: **ORCH-040**
|
||||
|
||||
Каждый критерий имеет чёткое условие PASS/FAIL. Задача считается принятой, когда
|
||||
**все** критерии = PASS.
|
||||
|
||||
## AC-1 — Артефакты создаются под uid хоста (корневой критерий)
|
||||
- **PASS**: после прогона тестовой задачи конвейером end-to-end новые tracked-файлы
|
||||
в `/home/slin/repos/orchestrator/docs/work-items/*` и в worktree
|
||||
(`/repos/_wt/...`) имеют владельца `slin:slin` (1000:1000).
|
||||
`ls -ld /home/slin/repos/orchestrator/docs/work-items/*` → НЕ `root:root`.
|
||||
- **FAIL**: появляются новые `root:root` tracked-файлы.
|
||||
|
||||
## AC-2 — git под slin работает без ручного chown
|
||||
- **PASS**: на хосте под `slin` `git -C /home/slin/repos/orchestrator pull`,
|
||||
`git status`, `git reset` выполняются без `Permission denied` /
|
||||
`insufficient permission for adding an object`.
|
||||
- **FAIL**: любая из команд падает на правах.
|
||||
|
||||
## AC-3 — claude-агенты стартуют (preflight ok)
|
||||
- **PASS**: `claude-auth`/preflight проходит; агент конвейера запускается и
|
||||
завершается `exit_code=0` (не `Not logged in`, не отказ доступа к creds).
|
||||
- **FAIL**: агент падает на авторизации/чтении `/home/slin/.claude`.
|
||||
|
||||
## AC-4 — docker.sock доступен (деплой не сломан)
|
||||
- **PASS**: из контейнера под новым uid `docker ps` / docker-операции деплоя
|
||||
(ORCH-36 путь) работают — доступ через gid 999 сохранён (`group_add: ["999"]`).
|
||||
- **FAIL**: docker-операции отваливаются (`permission denied` на сокете).
|
||||
|
||||
## AC-5 — SSH-деплой работает
|
||||
- **PASS**: ssh-ключи читаются из домашнего каталога нового uid; деплой-хук по ssh
|
||||
(`DEPLOY_SSH_*`) выполняется.
|
||||
- **FAIL**: ssh не находит/не читает ключи (маунт указывает на чужой HOME).
|
||||
|
||||
## AC-6 — Конвейер не сломан (без регресса)
|
||||
- **PASS**: тестовая задача проходит стадии без падения запуска конвейера; доступ к
|
||||
Plane/Gitea из агентов сохранён; `pytest tests/ -q` зелёный.
|
||||
- **FAIL**: конвейер встаёт / тесты падают.
|
||||
|
||||
## AC-7 — Проверено на staging ДО прода
|
||||
- **PASS**: изменение прогнано на staging (8501), `15-staging-log.md` →
|
||||
`staging_status:` положительный; прод-рестарт выполнен в окно тишины.
|
||||
- **FAIL**: изменение применено сразу на прод без staging-прогона.
|
||||
|
||||
## AC-8 — Документация обновлена (golden source)
|
||||
- **PASS**: `docs/operations/INFRA.md` (блок volumes / SSH target / uid рантайма)
|
||||
и `CHANGELOG.md` обновлены; ADR с выбором варианта и обоснованием создан в
|
||||
`06-adr/`. Reviewer подтверждает.
|
||||
- **FAIL**: код изменён, документация/ADR не обновлены.
|
||||
|
||||
## AC-9 — Прод-контейнер не уронен вне окна тишины
|
||||
- **PASS**: рестарт self выполнен без активных задач; конвейер enduro-trails не
|
||||
пострадал.
|
||||
- **FAIL**: рестарт во время активных задач / падение прод-инстанса.
|
||||
|
||||
## AC-10 — Host-prerequisites зафиксированы и выполнены
|
||||
- **PASS**: предусловия P-1…P-4 (TRZ §10 / BRD §8) описаны в ADR/INFRA как
|
||||
обязательная процедура Owner; P-1 (доступ uid 1000 к claude creds) фактически
|
||||
обеспечен — подтверждается прохождением AC-3.
|
||||
- **FAIL**: фикс применён без обеспечения доступа к creds (P-1) → preflight/конвейер
|
||||
падает; либо предусловия нигде не задокументированы.
|
||||
81
docs/work-items/ORCH-040/04-test-plan.yaml
Normal file
81
docs/work-items/ORCH-040/04-test-plan.yaml
Normal file
@@ -0,0 +1,81 @@
|
||||
work_item: ORCH-040
|
||||
description: >
|
||||
Инфра-фикс: контейнер/агенты не плодят root-файлы в хостовом репо.
|
||||
Часть проверок автоматизируема через pytest (валидация compose-конфига),
|
||||
часть — обязательные ops/integration проверки на staging и хосте (manual),
|
||||
т.к. касаются прав файловой системы хоста и рантайма docker.
|
||||
|
||||
tests:
|
||||
# --- Автоматизируемые (pytest, парсинг docker-compose.yml) ---
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: >
|
||||
docker-compose.yml: оба сервиса (orchestrator, orchestrator-staging)
|
||||
имеют user: "1000:1000" (при выборе Варианта 1).
|
||||
module: tests/test_orch040_compose.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: >
|
||||
docker-compose.yml: оба сервиса сохраняют group_add со значением "999"
|
||||
(доступ к docker.sock не потерян — МИНА 1).
|
||||
module: tests/test_orch040_compose.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: >
|
||||
docker-compose.yml: SSH-маунт согласован с HOME агента — target каталога
|
||||
.ssh лежит под /home/slin (а не /root/.ssh), для обоих сервисов.
|
||||
module: tests/test_orch040_compose.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: >
|
||||
launcher: HOME, форсимый в окружении агента и git_env, указывает на каталог,
|
||||
совместимый с SSH/claude-маунтами (/home/slin) — нет рассинхрона HOME vs uid.
|
||||
module: tests/test_orch040_compose.py
|
||||
expected: PASS
|
||||
|
||||
# --- Регресс существующего поведения ---
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: >
|
||||
Весь существующий набор тестов зелёный (нет регресса логики конвейера/launcher).
|
||||
module: tests/ # pytest tests/ -q
|
||||
expected: PASS
|
||||
|
||||
# --- Integration / ops (staging 8501, затем хост) ---
|
||||
- id: TC-06
|
||||
type: integration
|
||||
description: >
|
||||
На staging (8501) прогнать тестовую задачу конвейером end-to-end; артефакты
|
||||
worktree и docs создаются под 1000:1000 (НЕ root:root). Проверка AC-1.
|
||||
module: scripts/staging_check.py # + ls -ld на хосте
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: integration
|
||||
description: >
|
||||
После staging-прогона на хосте под slin: git -C /home/slin/repos/orchestrator
|
||||
pull/status/reset без Permission denied. Проверка AC-2.
|
||||
module: manual/host-check
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: integration
|
||||
description: >
|
||||
claude preflight/auth проходит под новым uid: агент стартует и завершается
|
||||
exit_code=0 (creds /home/slin/.claude читаются). Проверка AC-3 (МИНА 2).
|
||||
module: manual/staging-agent-run
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: integration
|
||||
description: >
|
||||
docker.sock доступен из контейнера под uid 1000 (docker ps работает) и
|
||||
ssh-деплой-хук выполняется. Проверка AC-4, AC-5 (МИНА 1 + SSH).
|
||||
module: manual/staging-deploy-path
|
||||
expected: PASS
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user