Фундамент тиража 10-common (эпик ORCH-10): платформа разворачивается на
новой инфре без правки кода — только env/конфиг. Каждый дефолт = боевому
значению (пустой .env => поведение 1:1, kill-switch-природа, NFR-2);
STAGE_TRANSITIONS/QG_CHECKS/check_*/machine-verdict/схема БД не тронуты.
- config: agent_home_dir / agent_git_name / git_email_domain / staging_port
(ADR-001 D2/D4); код-блокеры A1-A4 закрыты: plane_sync ссылки из
gitea_public_url+gitea_owner, launcher - единый agent_git_env() (x2 места),
self_deploy/post_deploy - HOME+домен из Settings (имена системных акторов -
платформенные литералы)
- image_freshness: staging_port из конфига + fail-closed guard
staging_port == прод-порт -> отказ ДО ssh/build (инвариант ORCH-058 AC-9
стал исполняемым); REPO= передаётся хуку явно обоими инвокерами (D7)
- SELF_HOSTING_REPO - нормативная платформенная константа (D3, пин-тест)
- compose: полная ${VAR:-default}-интерполяция (реестр B, карта D6); группа
ORCH-040 uid/gid/HOME/маунты двигается согласованно (build.args APP_*);
group_add "МИНА 1" сохранён x3; оба app-сервиса с явным command:
- Dockerfile: ARG APP_UID/APP_GID/APP_USER/APP_HOME (CMD exec-form 8500
сознательно не тронут - D5); deploy-hook: REPO="${REPO:-...}" (D1 реестра)
- секреты: stdlib scripts/gen_secrets.py (token_hex(32); печать по умолчанию;
--write никогда не перезаписывает существующий .env молча, exit=2;
перезапись только --force); .env.example дополнен до полноты ключей старта
- доки: новый docs/operations/REPLICATION.md (карта env, чек-лист секретов,
smoke-процедура с PASS/FAIL, границы 10-common/Lite/Bundled), INFRA.md,
README, CLAUDE.md, CHANGELOG
- анти-регресс: tests/test_no_host_hardcodes.py (tokenize-сканер запрещённых
литералов, config-модули - структурное исключение, allowlist пуст,
негативная самопроверка) + test_host_config_keys / test_infra_parametrization
/ test_secrets_gen / test_replication_smoke; согласованные структурные
правки test_orch040_compose (судит резолв дефолтов) и
test_deploy_hook_rollback_sim (REPO через env-override = контракт D7)
Полный регресс: 1764 passed.
Refs: ORCH-101
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
4.7 KiB
Python
100 lines
4.7 KiB
Python
"""ORCH-101 (TC-10, AC-3/AC-4): репликационная документация и smoke-процедура.
|
||
|
||
Структурные проверки: deployment-док `docs/operations/REPLICATION.md`
|
||
существует и несёт пошаговую smoke-процедуру с явными PASS/FAIL-критериями,
|
||
карту env-переменных, чек-лист секретов и границы 10-common vs Lite vs
|
||
Bundled; карта env в INFRA.md дополнена; CHANGELOG.md содержит запись
|
||
ORCH-101; гайд stateless (не предписывает перенос боевых данных/секретов).
|
||
Скрипт-обвязка ORCH-101 не вводилась (ADR-001 D9 — чистый runbook), поэтому
|
||
запускаемость проверяется только для генератора секретов (--help, без
|
||
сети/LLM).
|
||
"""
|
||
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||
REPLICATION = REPO_ROOT / "docs/operations/REPLICATION.md"
|
||
INFRA = REPO_ROOT / "docs/operations/INFRA.md"
|
||
CHANGELOG = REPO_ROOT / "CHANGELOG.md"
|
||
|
||
|
||
def _replication_text() -> str:
|
||
assert REPLICATION.is_file(), "docs/operations/REPLICATION.md отсутствует (AC-3)"
|
||
return REPLICATION.read_text(encoding="utf-8")
|
||
|
||
|
||
def test_replication_doc_has_smoke_procedure_with_pass_fail():
|
||
text = _replication_text()
|
||
# Каждый шаг smoke имеет явный критерий; маркеры вердикта присутствуют.
|
||
assert "PASS" in text and "FAIL" in text
|
||
# Ключевые кирпичи процедуры (D9): health-check, onboarding-CLI, очередь.
|
||
for brick in ("/health", "/queue", "onboard_project.py", "docker compose config"):
|
||
assert brick in text, f"smoke-кирпич {brick} не упомянут в REPLICATION.md"
|
||
# Минимальный сигнал «конвейер доехал»: стадия analysis + артефакты 01–04.
|
||
assert "analysis" in text
|
||
assert "01" in text and "04" in text
|
||
|
||
|
||
def test_replication_doc_covers_secrets_checklist():
|
||
text = _replication_text()
|
||
assert "gen_secrets.py" in text
|
||
for token in (
|
||
"ORCH_PLANE_WEBHOOK_SECRET",
|
||
"ORCH_GITEA_WEBHOOK_SECRET",
|
||
"ORCH_PLANE_API_TOKEN",
|
||
"ORCH_GITEA_TOKEN",
|
||
"ORCH_TELEGRAM_BOT_TOKEN",
|
||
):
|
||
assert token in text, f"секрет {token} не покрыт чек-листом"
|
||
# Нормативная строка stateless-тиража (AC-5): боевые секреты не копируются.
|
||
assert "не копиру" in text.lower()
|
||
|
||
|
||
def test_replication_doc_has_env_map_and_boundaries():
|
||
text = _replication_text()
|
||
for var in (
|
||
"ORCH_AGENT_HOME_DIR",
|
||
"ORCH_AGENT_GIT_NAME",
|
||
"ORCH_GIT_EMAIL_DOMAIN",
|
||
"ORCH_STAGING_PORT",
|
||
"ORCH_DOCKER_GID",
|
||
"ORCH_RUN_UID",
|
||
"ORCH_HOST_REPOS_DIR",
|
||
):
|
||
assert var in text, f"переменная {var} отсутствует в карте env REPLICATION.md"
|
||
# Границы тиража (анти-скоуп-крип Р-5) + платформенные конвенции (D3/D4).
|
||
assert "Lite" in text and "Bundled" in text
|
||
assert "orchestrator" in text # конвенция имени репо платформы
|
||
# Чек-лист обязывает задать реестр проектов на новом хосте (A9).
|
||
assert "ORCH_PROJECTS_JSON" in text
|
||
|
||
|
||
def test_replication_doc_is_stateless():
|
||
text = _replication_text().lower()
|
||
# Процедура не предписывает перенос БД/данных с боевого хоста.
|
||
assert "перенос" not in text or "не предполагает перенос" in text or "без перенос" in text
|
||
|
||
|
||
def test_infra_env_map_extended():
|
||
text = INFRA.read_text(encoding="utf-8")
|
||
for var in ("ORCH_AGENT_HOME_DIR", "ORCH_STAGING_PORT", "ORCH_DOCKER_GID"):
|
||
assert var in text, f"{var} отсутствует в карте env INFRA.md (AC-4)"
|
||
assert "REPLICATION.md" in text # перекрёстная ссылка на deployment-док
|
||
|
||
|
||
def test_changelog_has_orch_101_entry():
|
||
text = CHANGELOG.read_text(encoding="utf-8")
|
||
assert "ORCH-101" in text
|
||
|
||
|
||
def test_gen_secrets_runs_in_safe_mode():
|
||
"""Обвязка запускается без ошибок в безопасном режиме (--help: без сети,
|
||
без LLM, без записи файлов)."""
|
||
r = subprocess.run(
|
||
[sys.executable, str(REPO_ROOT / "scripts/gen_secrets.py"), "--help"],
|
||
capture_output=True, text=True, timeout=30,
|
||
)
|
||
assert r.returncode == 0, r.stderr
|