diff --git a/.github/workflows/app-smoke.yml b/.github/workflows/app-smoke.yml index 80e4fc8..b60e66e 100644 --- a/.github/workflows/app-smoke.yml +++ b/.github/workflows/app-smoke.yml @@ -8,10 +8,12 @@ on: - ".env.example" - "compose/**" - "config/versions.env" + - "Taskfile.yml" - "scripts/check-apps.ps1" - "scripts/check-compose.ps1" - "scripts/check-keycloak.ps1" - "scripts/start-local.ps1" + - "scripts/service-local.ps1" - "scripts/stop-local.ps1" permissions: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84b3970..651117d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,11 @@ jobs: - uses: azure/setup-helm@v5.0.1 with: version: v4.2.4 + - uses: go-task/setup-task@v2 + with: + version: 3.53.1 + - name: Проверить команды разработчика + run: task --list - name: Проверить Helm chart shell: pwsh run: ./scripts/check-config.ps1 diff --git a/AGENTS.md b/AGENTS.md index 5e4b0dd..f5449f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,9 +22,8 @@ ## Команды ```powershell -pwsh ./scripts/check-chart.ps1 -pwsh ./scripts/smoke-k3d.ps1 -pwsh ./scripts/check-docs.ps1 +task verify +task test:e2e ``` ## Правила изменений diff --git a/README.md b/README.md index 9d07bc4..0197ab3 100644 --- a/README.md +++ b/README.md @@ -21,22 +21,47 @@ - Helm 4.2.4; - k3d 5.9.0; - kubectl; +- Task 3.53.1; - Windows PowerShell 5.1 или PowerShell 7. -Создай локальный файл настроек и запусти инфраструктуру: +На Windows установи Task один раз: + +```powershell +winget install --id Task.Task --exact --version 3.53.1 +``` + +Открой новый терминал и выполни `task doctor`, чтобы проверить Docker Engine и Docker Compose. + +Создай локальный файл настроек и запусти только инфраструктуру без приложений: ```powershell Copy-Item .env.example .env -./scripts/start-local.ps1 -Observe +task infra:up +``` + +Поднять один сервис из соседней локальной репы вместе с обязательными зависимостями: + +```powershell +task service:up SERVICE=action-service ``` -Чтобы поднять проверяемый путь `Channel → Agent → Action → Gateway → Calendar`, используй: +После изменения кода пересобери только нужный сервис: ```powershell -./scripts/start-local.ps1 -Apps -./scripts/check-apps.ps1 +task service:restart SERVICE=action-service ``` +Чтобы поднять и проверить путь `Channel → Agent → Action → Gateway → Calendar`, используй: + +```powershell +task services:up +task test:e2e +``` + +`task status` показывает состояние, `task service:logs SERVICE=action-service` — логи. `task down` +сохраняет локальные данные. `task reset` удаляет только volumes текущего Compose project и всегда +требует явного подтверждения. + `check-apps.ps1` отправляет demo-команду в Channel Gateway, проверяет предложение, создаёт и подтверждает действие, ждёт Temporal workflow и проверяет, что Calendar MCP сохранил ровно одно событие с тем же `eventId`. @@ -81,7 +106,7 @@ Worker, Temporal UI и Linux Compose используют внутренний Все быстрые проверки: ```powershell -./scripts/check-config.ps1 +task verify ``` Полная проверка: diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..a759796 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,91 @@ +version: "3" + +silent: true + +vars: + POWERSHELL: '{{if eq OS "windows"}}powershell.exe{{else}}pwsh{{end}}' + POWERSHELL_ARGS: '{{if eq OS "windows"}}-NoProfile -ExecutionPolicy Bypass{{else}}-NoProfile{{end}}' + +tasks: + default: + desc: Показать доступные команды + cmds: + - task --list + + infra:up: + desc: Поднять PostgreSQL, Redpanda, Keycloak, Temporal и OPA без приложений + cmds: + - '{{.POWERSHELL}} {{.POWERSHELL_ARGS}} -File ./scripts/start-local.ps1' + + doctor: + desc: Проверить инструменты для локального Compose-режима + cmds: + - docker version --format "Docker Engine {{`{{.Server.Version}}`}}" + - docker compose version + + infra:observe: + desc: Поднять инфраструктуру вместе с локальной наблюдаемостью + cmds: + - '{{.POWERSHELL}} {{.POWERSHELL_ARGS}} -File ./scripts/start-local.ps1 -Observe' + + services:up: + desc: Собрать и поднять все локальные сервисы + cmds: + - '{{.POWERSHELL}} {{.POWERSHELL_ARGS}} -File ./scripts/start-local.ps1 -Apps' + + service:up: + desc: Собрать выбранный сервис и поднять его обязательные зависимости + requires: + vars: [SERVICE] + cmds: + - '{{.POWERSHELL}} {{.POWERSHELL_ARGS}} -File ./scripts/start-local.ps1 -Service "{{.SERVICE}}"' + + service:stop: + desc: Остановить выбранный сервис + requires: + vars: [SERVICE] + cmds: + - '{{.POWERSHELL}} {{.POWERSHELL_ARGS}} -File ./scripts/service-local.ps1 -Action Stop -Service "{{.SERVICE}}"' + + service:restart: + desc: Пересобрать и перезапустить выбранный сервис + requires: + vars: [SERVICE] + cmds: + - '{{.POWERSHELL}} {{.POWERSHELL_ARGS}} -File ./scripts/service-local.ps1 -Action Restart -Service "{{.SERVICE}}"' + + service:logs: + desc: Следить за логами выбранного сервиса + requires: + vars: [SERVICE] + vars: + TAIL: '{{default "100" .TAIL}}' + cmds: + - '{{.POWERSHELL}} {{.POWERSHELL_ARGS}} -File ./scripts/service-local.ps1 -Action Logs -Service "{{.SERVICE}}" -Tail {{.TAIL}}' + + status: + desc: Показать состояние локального окружения + cmds: + - '{{.POWERSHELL}} {{.POWERSHELL_ARGS}} -File ./scripts/service-local.ps1 -Action Status' + + test:e2e: + desc: Поднять полный срез и выполнить сквозной тест встречи + cmds: + - task: services:up + - '{{.POWERSHELL}} {{.POWERSHELL_ARGS}} -File ./scripts/check-apps.ps1' + + verify: + desc: Выполнить локальные проверки конфигурации и документации + cmds: + - '{{.POWERSHELL}} {{.POWERSHELL_ARGS}} -File ./scripts/check-config.ps1' + + down: + desc: Остановить локальное окружение без удаления данных + cmds: + - '{{.POWERSHELL}} {{.POWERSHELL_ARGS}} -File ./scripts/stop-local.ps1' + + reset: + desc: Остановить окружение и удалить только его локальные данные + prompt: Все локальные данные Portable Agent будут удалены. Продолжить? + cmds: + - '{{.POWERSHELL}} {{.POWERSHELL_ARGS}} -File ./scripts/stop-local.ps1 -DeleteData' diff --git a/docs/decisions/0003-taskfile-workflow.md b/docs/decisions/0003-taskfile-workflow.md new file mode 100644 index 0000000..2e016be --- /dev/null +++ b/docs/decisions/0003-taskfile-workflow.md @@ -0,0 +1,31 @@ +# ADR 0003: единые команды локальной разработки + +Статус: принято. + +## Контекст + +Compose требует несколько env-файлов, profiles и Windows override. Прямой запуск скриптов заставлял +разработчика помнить эти детали и привязывал основной сценарий к конкретной версии PowerShell. +Нужны одинаковые короткие команды для Windows, Linux, локального Compose и будущего k3d-контура. + +## Решение + +Используем Task 3.53.1 как тонкую кроссплатформенную точку входа. Taskfile не хранит бизнес-логику и +не заменяет Docker Compose, Helm или тестовые инструменты. Он вызывает короткие проверяемые скрипты. + +Поддерживаются отдельные сценарии: + +- инфраструктура без приложений; +- выбранный сервис с обязательными зависимостями; +- все сервисы; +- состояние, логи, перезапуск и остановка; +- полный локальный E2E; +- явный reset с подтверждением. + +Версия Task закреплена в CI. Taskfile валидируется в каждом pull request. + +## Последствия + +Разработчик изучает один набор команд вместо параметров Compose. Существующие скрипты остаются +обратимо совместимыми, но больше не являются главным пользовательским интерфейсом. Kubernetes-команды +будут добавлены в тот же Taskfile только после появления зелёного k3d developer smoke. diff --git a/docs/development.md b/docs/development.md index 7047728..8f5e7a8 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,5 +1,29 @@ # Разработка +## Обычный цикл + +Taskfile — единая точка входа. Внутренние PowerShell-скрипты остаются короткой реализацией команд, +но разработчику не нужно помнить Compose-файлы, profiles и env-файлы. +После установки Task выполни `task doctor`: команда заранее проверит Docker Engine и Docker Compose. + +Для работы над сервисом: + +1. Выполни `task infra:up`, чтобы поднять PostgreSQL, Redpanda, Keycloak, Temporal и OPA. +2. Запусти нужный сервис командой `task service:up SERVICE=` либо запусти его из IDE против + опубликованных локальных портов инфраструктуры. +3. После изменения кода выполни `task service:restart SERVICE=`. +4. Посмотри состояние через `task status`, логи — через `task service:logs SERVICE=`. +5. Перед pull request выполни `task test:e2e`. + +Допустимые имена: `channel-gateway`, `agent-runtime`, `action-service`, `mcp-gateway` и +`calendar-mcp`. Неизвестное имя отклоняется до вызова Docker. + +`task test:e2e` поднимает полный локальный срез, ждёт healthchecks и проверяет путь от текста до +сохранённого события. Следующий пакет передаст запуск black-box сценариев репозиторию `test-lab`. + +`task down` не удаляет данные. `task reset` предназначен для явного пересоздания тестового состояния +и запрашивает подтверждение. + Работа идёт по TDD: 1. проверка описывает ожидаемый Kubernetes-ресурс; diff --git a/docs/runbook.md b/docs/runbook.md index 014a04b..1305fcb 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -17,12 +17,12 @@ ## Приложения не запускаются -1. Выполни `./scripts/check-compose.ps1` из Windows PowerShell 5.1 или PowerShell 7. +1. Выполни `task status`, затем `task verify`. 2. Проверь, что рядом лежат репозитории `portable-agent-channel-gateway`, `portable-agent-agent-runtime`, `portable-agent-action-service`, `portable-agent-mcp-gateway` и `portable-agent-calendar-mcp`, либо задай их `*_CONTEXT` в `.env`. 3. Если Keycloak сообщает о старом fixture, локально выполни - `./scripts/stop-local.ps1 -DeleteData`. Команда удаляет только volumes этого Compose project. + `task reset`. Команда запрашивает подтверждение и удаляет только volumes этого Compose project. 4. Проверь `/health/live` Channel Gateway и Agent Runtime, `/actuator/health/readiness` Action Service и `/health` двух MCP-сервисов. 5. Не включай Calendar test API вне локального профиля `apps`. @@ -30,7 +30,7 @@ После запуска проверь весь backend-путь одной командой: ```powershell -./scripts/check-apps.ps1 +task test:e2e ``` Она отправляет текст через Channel Gateway, затем создаёт действие с тем же случайным `requestKey`, diff --git a/mkdocs.yml b/mkdocs.yml index 7420bae..e8c5b33 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,3 +13,4 @@ nav: - Решения: - Отдельный deploy-репозиторий: decisions/0001-deploy-repository.md - Платформа разработки: decisions/0002-developer-platform.md + - Единые команды разработчика: decisions/0003-taskfile-workflow.md diff --git a/scripts/check-compose.ps1 b/scripts/check-compose.ps1 index 9bc9e66..f122023 100644 --- a/scripts/check-compose.ps1 +++ b/scripts/check-compose.ps1 @@ -117,6 +117,47 @@ if ($startScript -notmatch '\[switch\]\$Apps' -or $startScript -notmatch '"apps" if ($startScript -notmatch 'compose/apps.local.yaml' -or $startScript -notmatch 'up -d --build --wait') { throw "Локальные приложения должны собираться из соседних репозиториев." } +$taskfilePath = "Taskfile.yml" +if (-not (Test-Path -LiteralPath $taskfilePath)) { + throw "Нет единой точки входа Taskfile.yml для локальной разработки." +} +$taskfile = Get-Content -Raw -LiteralPath $taskfilePath +foreach ($taskName in @( + "infra:up", + "services:up", + "service:up", + "service:stop", + "service:restart", + "service:logs", + "doctor", + "status", + "test:e2e", + "down", + "reset" +)) { + if ($taskfile -notmatch "(?m)^ $([regex]::Escape($taskName)):") { + throw "В Taskfile нет команды $taskName." + } +} +if ($taskfile -notmatch 'scripts/start-local\.ps1' ` + -or $taskfile -notmatch 'scripts/service-local\.ps1' ` + -or $taskfile -notmatch 'scripts/check-apps\.ps1' ` + -or $taskfile -notmatch '(?m)^ prompt:') { + throw "Taskfile не связывает команды запуска, управления, E2E и безопасного reset." +} +if ($startScript -notmatch '\[string\]\$Service' -or $startScript -notmatch '\$Service') { + throw "start-local должен уметь запускать один выбранный сервис." +} +$serviceScriptPath = "scripts/service-local.ps1" +if (-not (Test-Path -LiteralPath $serviceScriptPath)) { + throw "Нет безопасного управления отдельным локальным сервисом." +} +$serviceScript = Get-Content -Raw -LiteralPath $serviceScriptPath +foreach ($action in @("Status", "Stop", "Restart", "Logs")) { + if ($serviceScript -notmatch [regex]::Escape('"' + $action + '"')) { + throw "service-local не поддерживает команду $action." + } +} $appsOverride = Get-Content -Raw -LiteralPath "compose/apps.local.yaml" foreach ($image in @("portable-agent/channel-gateway:local", "portable-agent/agent-runtime:local", "portable-agent/action-service:local", "portable-agent/mcp-gateway:local", "portable-agent/calendar-mcp:local")) { if ($appsOverride -notmatch [regex]::Escape($image)) { diff --git a/scripts/check-docs.ps1 b/scripts/check-docs.ps1 index fd872dd..31bd4c9 100644 --- a/scripts/check-docs.ps1 +++ b/scripts/check-docs.ps1 @@ -9,7 +9,8 @@ $requiredFiles = @( "docs/architecture.md", "docs/development.md", "docs/runbook.md", - "docs/decisions/0001-deploy-repository.md" + "docs/decisions/0001-deploy-repository.md", + "docs/decisions/0003-taskfile-workflow.md" ) $missing = $requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Leaf) } diff --git a/scripts/service-local.ps1 b/scripts/service-local.ps1 new file mode 100644 index 0000000..b48905b --- /dev/null +++ b/scripts/service-local.ps1 @@ -0,0 +1,39 @@ +param( + [Parameter(Mandatory = $true)] + [ValidateSet("Status", "Stop", "Restart", "Logs")] + [string]$Action, + [ValidateSet("channel-gateway", "agent-runtime", "action-service", "mcp-gateway", "calendar-mcp")] + [string]$Service, + [ValidateRange(1, 10000)] + [int]$Tail = 100 +) + +$ErrorActionPreference = "Stop" +if ($Action -ne "Status" -and -not $Service) { + throw "Для команды $Action укажи -Service." +} + +$envFiles = @("--env-file", ".env.example", "--env-file", "config/versions.env") +if (Test-Path .env) { $envFiles += @("--env-file", ".env") } +$composeFiles = @("-f", "compose/compose.yaml") +$runningOnWindows = $PSVersionTable.PSEdition -eq "Desktop" -or $IsWindows +if ($runningOnWindows) { $composeFiles += @("-f", "compose/windows.local.yaml") } +$composeFiles += @("-f", "compose/apps.local.yaml") +$profiles = @("--profile", "core", "--profile", "apps") + +switch ($Action) { + "Status" { + & docker compose @envFiles @composeFiles @profiles ps + } + "Stop" { + & docker compose @envFiles @composeFiles @profiles stop $Service + } + "Restart" { + & docker compose @envFiles @composeFiles @profiles up -d --build --wait $Service + } + "Logs" { + & docker compose @envFiles @composeFiles @profiles logs --tail $Tail --follow $Service + } +} + +if ($LASTEXITCODE -ne 0) { throw "Команда $Action для локального окружения завершилась с ошибкой." } diff --git a/scripts/start-local.ps1 b/scripts/start-local.ps1 index 74a0461..54601ac 100644 --- a/scripts/start-local.ps1 +++ b/scripts/start-local.ps1 @@ -1,5 +1,11 @@ -param([switch]$Observe, [switch]$Apps) +param( + [switch]$Observe, + [switch]$Apps, + [ValidateSet("channel-gateway", "agent-runtime", "action-service", "mcp-gateway", "calendar-mcp")] + [string]$Service +) $ErrorActionPreference = "Stop" +if ($Apps -and $Service) { throw "Используй -Apps или -Service, но не оба параметра одновременно." } $envFiles = @("--env-file", ".env.example", "--env-file", "config/versions.env") if (Test-Path .env) { $envFiles += @("--env-file", ".env") } $composeFiles = @("-f", "compose/compose.yaml") @@ -41,5 +47,10 @@ if ($Apps) { & docker compose @envFiles @composeFiles -f compose/apps.local.yaml ` @appProfiles up -d --build --wait --scale temporal-namespace=0 if ($LASTEXITCODE -ne 0) { throw "Приложения локального среза не запустились." } +} elseif ($Service) { + $appProfiles = @($coreProfiles + @("--profile", "apps")) + & docker compose @envFiles @composeFiles -f compose/apps.local.yaml ` + @appProfiles up -d --build --wait $Service + if ($LASTEXITCODE -ne 0) { throw "Сервис $Service и его зависимости не запустились." } } Write-Host "Локальная инфраструктура готова. Для остановки: ./scripts/stop-local.ps1"