# План реализации xboctploy Источник требований: `docs/pdr-adr.md` (PRD + ADR-001..005). Статус: утверждено — конфиг локальный с фолбэком на глобальный, scope полный (включая .env + ssh-agent), cargo-проект в корне репо. --- ## 1. Зафиксированные решения | Вопрос | Решение | | ------------------------ | ------------------------------------------------------------------------------------------------------- | | Расположение deploy.toml | Сначала `./deploy.toml` из текущей папки; если не найден — глобальный `~/.config/xboctploy/deploy.toml` | | Объём работ | Полный roadmap включительно (.env + ssh-agent) | | Размещение проекта | Cargo-проект в корне репозитория xboctploy/ | | Имя бинарника | `xboctploy` | | Модель выполнения | Синхронная (ADR-003), без tokio | Локальный конфиг приоритетен — так тулза тестируется рядом с проектом, а глобальный остаётся для запуска «из любой папки». ## 2. Целевая структура ```txt Cargo.toml src/ main.rs # вход, anyhow-ошибки, коды выхода (0 успех / 1 fail-fast) cli.rs # clap derive: xboctploy [--config PATH] [--dry-run] [--list] config.rs # парсинг и валидация deploy.toml (serde + toml) local.rs # локальные команды, fail-fast ssh.rs # ssh2-config (~/.ssh/config), auth по ключу, exec удалённых команд sync.rs # SFTP: файлы + рекурсивная загрузка папок deploy.rs # оркестрация Local -> Sync -> Remote docs/ pdr-adr.md implementation-plan.md ``` ## 3. Схема deploy.toml ```toml [projects.my-node-app] workdir = "~/code/my-node-app" # где выполняются локальные команды host = "my-vps" # алиас из ~/.ssh/config или IP user = "deploy" port = 22 key_path = "~/.ssh/id_rsa" # опционально, дефолт ~/.ssh/id_rsa [projects.my-node-app.local] commands = ["npm ci", "npm run build"] [[projects.my-node-app.sync]] source = "dist" # файл или папка (относительно workdir) target = "/var/www/node-app" # абсолютный путь на VPS [projects.my-node-app.remote] commands = ["pm2 restart node-app"] ``` В Rust: `Config { projects: HashMap }`. Валидация: host обязателен, commands не пустые для секций, которые присутствуют; неизвестные поля — ошибка (serde deny_unknown_fields), чтобы опечатки ловились сразу. ## 4. Этапы реализации ### Этап 1 — Каркас + конфиг 1. `cargo init`, прописать зависимости (без фиксации версий): - clap (feature derive) - serde (feature derive), toml - anyhow - ssh2, ssh2-config - colored, walkdir, home 2. src/cli.rs: подкоманда по умолчанию — имя проекта; флаги --config, --dry-run, --list. 3. src/config.rs: структуры + резолв конфига по цепочке: `--config PATH` -> `./deploy.toml` -> `~/.config/xboctploy/deploy.toml`; если ничего не найдено — понятная ошибка. Развёртывание `~` через крейт home. 4. `--list`: печатает имена проектов из конфига. 5. Юнит-тесты: валидный TOML, битый TOML, отсутствие файла, развёртывание путей, приоритет локального конфига над глобальным. Критерий приёмки: `xboctploy --list` показывает проекты из тестового конфига; битый конфиг даёт понятную ошибку с именем поля. ### Этап 2 — Локальные команды 1. src/local.rs: последовательный запуск commands в workdir. 2. Кроссплатформенность спавна: Windows — `cmd /C `, Unix — `sh -c ` (иначе npm/npx не резолвятся в .cmd на Windows). 3. stdout/stderr наследуются в терминал; ненулевой exit code -> немедленный Err с текстом упавшей команды (ADR-005, fail-fast). 4. --dry-run: печатает список команд без запуска. Критерий приёмки: на тестовом проекте `npm run build` выполняется; падающий шаг останавливает деплой с красной ошибкой и exit code 1. ### Этап 3 — SSH + удалённые команды 1. src/ssh.rs: резолв хоста через ssh2-config (алиасы ~/.ssh/config: hostname, user, port, identityfile), затем явные значения из deploy.toml поверх конфига ssh. 2. Подключение: TCP -> handshake -> userauth_pubkey_file по ключу (key_path или ~/.ssh/id_rsa). 3. exec удалённой команды через ChannelSession: стриминг stdout/stderr, проверка channel.exit_status(); != 0 -> Err со stderr (fail-fast). Критерий приёмки: `pm2 restart node-app` (или echo) выполняется на реальной VPS по ключу; неверный ключ/хост даёт читаемую ошибку, а не панику. ### Этап 4 — SFTP-синхронизация 1. src/sync.rs: открытие SFTP-сесси поверх установленной SSH-сессии. 2. Одиночный файл: sftp.open/sendfile -> запись в target. 3. Рекурсивная папка: walkdir по source; для каждой подпапки удалённый mkdir с игнорированием ошибки "already exists" (аналог mkdir -p); файлы поверх. 4. Лог: `[SFTP] transferring dist/ -> /var/www/node-app ... Done!`, счётчик файлов/байт. Критерий приёмки: dist/ целиком появляется на VPS, повторный деплой не падает на существующих папках. ### Этап 5 — Оркестрация + UX 1. src/deploy.rs: пайплайн local -> sync -> remote, fail-fast между фазами. 2. Цветной вывод (colored) в формате PRD: - зелёный [Local] ... Success! - синий [SFTP] ... Done! - жёлтый [Remote] ... Success! - `Deploy successful!` / красный блок ошибки с stderr. 3. Тайминги каждого шага (Instant::now). 4. Опционально: indicatif progress-bar на этапе передачи файлов. Критерий приёмки: вывод визуально совпадает с примером UX из PRD; --dry-run печатает весь план всех трёх фаз без единого действия. ### Этап 6 — Продвинутый уровень 1. `.env` из workdir: ручной парсер (~20 строк, KEY=VALUE, игнор комментариев), переменные инжектятся в env локальных дочерних процессов; .env не заливается на VPS, если не указан явно в sync. 2. ssh-agent: ssh2 agent_connect -> agent_list_identities -> userauth_agent. 3. Цепочка аутентификации: ssh-agent -> ключевой файл -> понятная ошибка. 4. Флаг переопределения конфига --config уже есть с этапа 1. Критерий приёмки: агент в цепочке используется первым; .env-переменные видны внутри локальных команд сборки. ### Этап 7 — Финализация 1. `cargo fmt`, `cargo clippy -- -D warnings`, `cargo test`. 2. `cargo build --release`. 3. README: установка (cargo install --path .), схема конфига, примеры. 4. Smoke-тест на реальной VPS вручную (владелец): node-проект end-to-end. ## 5. Отклонения от ADR-002 (минимальные, оправданные) | Крейт | Причина | | ----------------- | ---------------------------------------------- | | walkdir | рекурсивное копирование папок по SFTP (этап 4) | | colored | цветные логи требуются PRD раздел 3 (этап 5) | | home | развёртывание `~` в путях конфига | | indicatif (опция) | progress-bar, только если понадобится | Всё остальное — строго по ADR-002. Асинхронность не вводим (ADR-003). ## 6. Стратегия верификации - Юнит-тесты: config (парсинг, валидация, пути, приоритет локального конфига) — этап 1; парсер .env — этап 6. - Интеграционный smoke-тест против реальной VPS — вручную владельцем (этап 7); при желании позже добавляется test с гейтом XBOCTPLOY_TEST_HOST. - Линтеры на каждом этапе: cargo clippy -D warnings + cargo fmt --check.