# CI на Gitea + Woodpecker: изолированный контур Репозиторий использует **Woodpecker CI 3.x**. Конфигурация — в `.woodpecker/`: | Workflow | Событие | Что делает | |---|---|---| | `check` | push, pull_request | тесты, ktlint, detekt, `evidenceCheck` | | `smoke` | push, pull_request | после `check`: полный стек, health, вход админа, один курс | | `release` | tag, manual | миграции, mutation, load, Chromium/Firefox/WebKit E2E, финальный evidence | ## 1. Модель изоляции CI **не должен работать на хосте LMS и не должен иметь доступ к production**. Разработчики не считаются доверенными: пайплайн может содержать произвольный код, включая `privileged`-контейнеры. Схема: ``` Gitea + Woodpecker server отдельная CI VM ┌───────────────────────┐ ┌──────────────────────────────┐ │ Gitea (production) │ webhook │ Woodpecker agent (Docker) │ │ Woodpecker server │◄────────►│ └─ pipeline step │ │ │ gRPC │ └─ nested dockerd (dind)│ └───────────────────────┘ │ ├─ postgres │ │ ├─ LMS-сервисы │ │ └─ Playwright │ └──────────────────────────────┘ нет prod-сети и секретов ``` Правила: - **Отдельная VM** (или отдельный физический хост) для агента. Не запускайте агент на сервере LMS/Gitea. - На VM **нет** production-БД, production-томов, deploy-ключей и `LMS_INTERNAL_TOKEN`/паролей production. Только одноразовые тестовые значения, которые создаёт сам пайплайн. - Сетевой firewall: VM может ходить в Gitea/Woodpecker и к registry/пакетным зеркалам; **не** может в production-подсеть и к production-БД. - VM должна пересоздаваться из чистого снапшота после релиза или по расписанию. Всё, что осталось на диске, считается недоверенным. - Пайплайн выполняется в образе `lms-ci:1` (на базе `docker:27-dind`) с **собственным nested dockerd**. Хост-Docker сокет в шаг не монтируется. `privileged` нужен только этому вложенному демону; на отдельной VM это допустимо. - Агент помечен `isolation=dedicated-vm`; workflows требуют эту метку. Это не даёт им случайно попасть на агент, стоящий рядом с production. - В Woodpecker не должно быть production-secrets. Если позже понадобится деплой — отдельный защищённый агент/пайплайн с ручным подтверждением, не этот. > Важно: `privileged`-шаг может скомпрометировать **VM**, но не production. > Именно поэтому VM обязательна. Не ослабляйте это правило. ## 2. Требования к CI VM - Docker Engine + Compose v2. - 4+ CPU, 8+ GB RAM, 40+ GB диска (Gradle, Docker-образы, браузеры). - Исходящий доступ к Gitea, registry и пакетным зеркалам. - Нет входящих портов, кроме SSH для администрирования. - Нет монтирования production-томов и нет production-сетей. ## 3. Woodpecker server Server можно держать рядом с Gitea (он не выполняет код пайплайнов). Пример `docker-compose.woodpecker.yml` (секреты не коммитить): ```yaml services: woodpecker-server: image: woodpeckerci/woodpecker-server:v3 restart: unless-stopped ports: - "127.0.0.1:8000:8000" # UI за nginx - "127.0.0.1:9000:9000" # gRPC для агента environment: WOODPECKER_OPEN: "false" WOODPECKER_HOST: ${WOODPECKER_HOST:?https://ci.example.ru} WOODPECKER_GITEA: "true" WOODPECKER_GITEA_URL: ${GITEA_URL:?https://git.example.ru} WOODPECKER_GITEA_CLIENT: ${GITEA_CLIENT:?} WOODPECKER_GITEA_SECRET: ${GITEA_SECRET:?} WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET:?} WOODPECKER_ADMIN: ${WOODPECKER_ADMIN:?admin} volumes: - woodpecker-data:/var/lib/woodpecker volumes: woodpecker-data: ``` 1. В Gitea создайте OAuth2-приложение: redirect URI `https://ci.example.ru/authorize`. 2. Заполните `GITEA_CLIENT`, `GITEA_SECRET`, `WOODPECKER_AGENT_SECRET` (например, `openssl rand -hex 32`), `WOODPECKER_HOST`, `GITEA_URL`. 3. Если Gitea и Woodpecker на одном хосте, разрешите webhook на loopback в `/etc/gitea/conf/app.ini`: ```ini [webhook] ALLOWED_HOST_LIST = external,loopback ``` Проверьте, что Gitea отдаёт API-страницы не меньше 50 элементов. 4. Поднимите server и зайдите в UI через Gitea-аккаунт. 5. Включите репозиторий LMS в Woodpecker (Settings → Repositories). 6. В настройках репозитория включите **Trusted** (нужно для `privileged` и volumes). Доверять можно только этому приватному репозиторию. ## 4. Агент на отдельной VM Агент — в Docker, backend `docker`, с доступом к Docker daemon **этой VM**. Шаги пайплайна получают не хостовый сокет, а собственный nested dockerd. `docker-compose.agent.yml` на CI VM: ```yaml services: woodpecker-agent: image: woodpeckerci/woodpecker-agent:v3 restart: unless-stopped environment: WOODPECKER_SERVER: ${WOODPECKER_SERVER:?https://ci.example.ru:9000} WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET:?} WOODPECKER_BACKEND: docker WOODPECKER_MAX_WORKFLOWS: "1" # Только этот агент должен брать workflows с этой меткой. WOODPECKER_AGENT_LABELS: isolation=dedicated-vm volumes: - /var/run/docker.sock:/var/run/docker.sock ``` ```bash docker compose -f docker-compose.agent.yml up -d docker compose -f docker-compose.agent.yml logs -f ``` Дополнительно в UI откройте **Settings → Agents** и задайте агенту фильтр `isolation=dedicated-vm` — это серверная проверка, которую агент не может подделать. **Общего кэша между пайплайнами нет.** Каждый шаг получает чистый контейнер, а вложенный Docker storage живёт только внутри него. Это важно: разработчик не может оставить артефакт, который повлияет на следующий pipeline или релиз. Пайплайны используют предварительно прогретый образ `lms-ci:1`. Соберите его на VM из **проверенного администратором** коммита до первого запуска: ```bash git checkout <проверенный-коммит> ./scripts/ci-build-image.sh lms-ci:1 ``` Сборка образа выполняет Gradle-код из репозитория. Не собирайте образ из непроверенной ветки/PR: это запуск недоверенного кода на CI VM. Образ содержит Docker-in-Docker, Java 21, Python, bash, Node и прогретый Gradle-кэш; исходники всё равно берутся из checkout. Пересобирайте образ при изменении зависимостей или базовых версий. Не монтируйте в шаги общие writable-кэши: это канал влияния одного пайплайна на другой. ## 5. Как устроены workflows - `check`, `smoke` и `release` требуют `labels.isolation=dedicated-vm`. - `smoke` ждёт успешного `check` (`depends_on`). - Все workflows имеют `concurrency: {limit: 1, group: lms-ci}`, потому что smoke/release поднимают стек на портах 8080–8090. - `check` и `smoke`/`release` используют `lms-ci:1` + `./scripts/ci-dind.sh`: скрипт поднимает **вложенный** dockerd. Образ собирается из `ci/Dockerfile`. - `release` перед миграционным тестом делает `git fetch` master: CI-клон может быть shallow. - `smoke`/`release` не получают хост-Docker сокет; `docker compose` работает с вложенным демоном. ## 6. Первый прогон 0. На CI VM соберите образ: `./scripts/ci-build-image.sh lms-ci:1`. 1. Push в ветку или MR → `check`, затем `smoke`. 2. Убедитесь, что оба workflow зелёные и выполнялись на агенте с меткой `isolation=dedicated-vm`. 3. Проверьте ручной запуск: UI → Run pipeline → event `manual` → workflow `release`. 4. Для релиза: `git tag vX.Y.Z && git push origin vX.Y.Z` → `release`. 5. После релиза/по расписанию пересоздайте VM из чистого снапшота. Если pipeline не появляется: - репозиторий включён в Woodpecker; - у агента и server совпадает `WOODPECKER_AGENT_SECRET`; - агент видит Docker: `docker compose version` на VM; - метка агента `isolation=dedicated-vm` и серверный фильтр совпадают; - путь конфигурации проекта — `.woodpecker/` (по умолчанию); - образ `lms-ci:1` собран на VM (иначе шаг не стартует); - репозиторий помечен trusted, иначе `privileged` будет отклонён. ## 7. Чек-лист безопасности - [ ] Агент не на production-хосте. - [ ] На VM нет production-секретов, БД и томов. - [ ] Firewall запрещает VM → production. - [ ] В Woodpecker нет production-secrets. - [ ] Включён trusted только для LMS, не для чужих репозиториев. - [ ] `WOODPECKER_AGENT_SECRET` уникален и хранится вне репозитория. - [ ] VM пересоздаётся из снапшота; кэш не считается доверенным. - [ ] `privileged` разрешён только на этой VM, не на общем runner.