From f3d614bb332b3ec55430a2dd2d5eae31f0ce3a28 Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Wed, 28 Oct 2026 22:50:23 +0300 Subject: [PATCH] =?UTF-8?q?ci:=20=D0=B8=D0=B7=D0=BE=D0=BB=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BF=D0=B0=D0=B9=D0=BF=D0=BB?= =?UTF-8?q?=D0=B0=D0=B9=D0=BD=D1=8B=20=D0=BD=D0=B0=20=D0=BE=D1=82=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D0=B9=20VM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Woodpecker workflows требуют агент с меткой isolation=dedicated-vm - шаги выполняются в lms-ci:1 с nested dockerd, без хост-Docker сокета - ci/Dockerfile: прогретый immutable-образ (Java, Python, Node, Gradle-кэш) - ci-dind.sh поднимает dockerd внутри privileged-шага; ci-build-image.sh собирает образ - deploy/WOODPECKER.md: отдельная VM, firewall, отсутствие production-секретов, trusted repo, чек-лист безопасности --- .dockerignore | 1 + .woodpecker/check.yaml | 18 +- .woodpecker/release.yaml | 16 +- .woodpecker/smoke.yaml | 13 +- README.md | 5 +- ci/Dockerfile | 24 +++ deploy/DEPLOY.md | 5 +- deploy/WOODPECKER.md | 240 ++++++++++++++++----------- docs/16-stage6-report.md | 13 +- docs/17-handoff.md | 19 ++- docs/evidence/baseline-2026-10-27.md | 6 +- scripts/ci-build-image.sh | 9 + scripts/ci-dind.sh | 47 ++++++ scripts/ci-preflight.sh | 2 +- scripts/ldd-browser.sh | 6 +- 15 files changed, 287 insertions(+), 137 deletions(-) create mode 100644 ci/Dockerfile create mode 100755 scripts/ci-build-image.sh create mode 100755 scripts/ci-dind.sh diff --git a/.dockerignore b/.dockerignore index 4fda28d..a7439d3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,6 +10,7 @@ oldLMS .env .env.prod node_modules +e2e __pycache__ *.pyc *.iml diff --git a/.woodpecker/check.yaml b/.woodpecker/check.yaml index 3f0b9fa..0793b8b 100644 --- a/.woodpecker/check.yaml +++ b/.woodpecker/check.yaml @@ -1,18 +1,22 @@ -# Merge gate: тесты, линтеры и evidence. -# Выполняется на Woodpecker-агенте с local backend: нужны Java 21, Docker -# (Testcontainers), Docker Compose v2, Python 3 и bash. +# Merge gate: тесты, линтеры и evidence в изолированном Docker-in-Docker. +# Агент с меткой isolation=dedicated-vm работает на отдельной VM; хост-Docker +# сокет в шаг не монтируется. +labels: + isolation: dedicated-vm + when: - event: [push, pull_request] steps: - name: check - image: bash + image: lms-ci:1 + privileged: true + environment: + DOCKER_HOST: unix:///var/run/docker.sock commands: - - ./scripts/ci-preflight.sh + - ./scripts/ci-dind.sh - ./gradlew --no-daemon test ktlintCheck detekt evidenceCheck -# Один общий слот: smoke/release поднимают реальный стек на localhost, -# параллельный запуск занял бы порты 8080–8090. concurrency: limit: 1 group: lms-ci diff --git a/.woodpecker/release.yaml b/.woodpecker/release.yaml index 8a064c5..18e1362 100644 --- a/.woodpecker/release.yaml +++ b/.woodpecker/release.yaml @@ -1,12 +1,22 @@ -# Полный release gate: тесты, миграции, mutation, runtime/scale и -# Chromium/Firefox/WebKit E2E. Запускается на тег или вручную. +# Полный release gate в изолированном Docker-in-Docker-шаге на отдельной VM. +# Запускается на тег или вручную. Хост-Docker сокет не монтируется. +labels: + isolation: dedicated-vm + when: - event: [tag, manual] steps: - name: release - image: bash + image: lms-ci:1 + privileged: true + environment: + DOCKER_HOST: unix:///var/run/docker.sock commands: + # ldd-migration-upgrade.sh сравнивает ветку с master; в CI клон может быть shallow. + - git fetch --unshallow origin || true + - git fetch origin master:refs/heads/master || true + - ./scripts/ci-dind.sh - cd e2e && npm ci && cd .. - ./scripts/ci-preflight.sh --release - ./scripts/ldd-release.sh diff --git a/.woodpecker/smoke.yaml b/.woodpecker/smoke.yaml index 760f887..83c396b 100644 --- a/.woodpecker/smoke.yaml +++ b/.woodpecker/smoke.yaml @@ -1,5 +1,8 @@ -# Дымовой тест на каждый push/MR: полный стек, health, вход админа, -# один доменный сценарий. Полный release gate — в release.yaml. +# Дымовой тест на каждый push/MR в изолированном Docker-in-Docker-шаге. +# Шаг сам поднимает dockerd внутри себя, поэтому не получает хост-Docker сокет. +labels: + isolation: dedicated-vm + when: - event: [push, pull_request] @@ -8,8 +11,12 @@ depends_on: steps: - name: smoke - image: bash + image: lms-ci:1 + privileged: true + environment: + DOCKER_HOST: unix:///var/run/docker.sock commands: + - ./scripts/ci-dind.sh - ./scripts/ci-preflight.sh - ./scripts/ldd-smoke.sh diff --git a/README.md b/README.md index 6e1486e..79ea262 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ progress `:8083`, web `:8090`. Интерфейс: `http://localhost:8090`. ./scripts/ldd-browser.sh # Playwright E2E: Chromium/Firefox/WebKit ./scripts/ldd-release.sh # полный release gate: test/upgrade/mutation/runtime/E2E ./scripts/ci-preflight.sh --release # проверка окружения runner без установки +./scripts/ci-build-image.sh # прогретый CI-образ lms-ci:1 для Woodpecker ./scripts/legacy-assets.sh # инвентаризация бесхозных файлов на стенде (admin) ``` @@ -88,8 +89,8 @@ progress `:8083`, web `:8090`. Интерфейс: `http://localhost:8090`. workflow `check` (`test ktlintCheck detekt evidenceCheck`) и `smoke` (`scripts/ldd-smoke.sh`) на каждом push/MR; полный `release` — `scripts/ldd-release.sh` — на тег или вручную. Smoke/release идут на - Woodpecker-агенте с local backend: Java 21, Docker Compose v2, Python 3, - Node и Chromium/Firefox/WebKit. Установка — + отдельной CI VM: Woodpecker Docker backend, nested dind, Java 21, + Docker Compose v2, Python 3, Node и Chromium/Firefox/WebKit. Установка — [`deploy/WOODPECKER.md`](deploy/WOODPECKER.md). Baseline: **51 claim, 346 тестов, 18/18 browser E2E**. diff --git a/ci/Dockerfile b/ci/Dockerfile new file mode 100644 index 0000000..e803ea8 --- /dev/null +++ b/ci/Dockerfile @@ -0,0 +1,24 @@ +# Предварительно прогретый CI-образ для Woodpecker. +# Сборка на CI VM (доверенный администратором коммит): +# ./scripts/ci-build-image.sh lms-ci:1 +# Образ содержит только зависимости; исходники в пайплайне берутся из checkout. +FROM docker:27-dind + +RUN apk add --no-cache openjdk21-jdk python3 bash + +WORKDIR /opt/lms-warmup + +COPY gradle gradle +COPY gradlew settings.gradle.kts build.gradle.kts gradle.properties ./ +COPY buildSrc buildSrc +COPY config config +COPY libs libs +COPY services services +COPY web web + +# Прогреваем Gradle-кэш и плагины. Это ускоряет check/smoke/release, +# при этом кэш остаётся внутри неизменяемого образа, а не в общем volume. +RUN ./gradlew --no-daemon testClasses --console=plain + +# Исходники нужны были только для прогрева кэша; в образе их не оставляем. +RUN rm -rf /opt/lms-warmup diff --git a/deploy/DEPLOY.md b/deploy/DEPLOY.md index 70e5509..03dd80b 100644 --- a/deploy/DEPLOY.md +++ b/deploy/DEPLOY.md @@ -150,8 +150,9 @@ workflow `check` (`./gradlew --no-daemon test ktlintCheck detekt evidenceCheck`) курс). Полный workflow `release` (`./scripts/ldd-release.sh`) обязателен для тегов и запускается вручную для веток. Smoke/release начинаются с `scripts/ci-preflight.sh`, который проверяет Java 21, Docker Compose v2, -Python 3, Node и Playwright. Агент — local backend на хосте с Docker: -установка и systemd-юнит — [`WOODPECKER.md`](WOODPECKER.md). +Python 3, Node и Playwright. Агент — Docker backend на отдельной CI VM, +шаги — в nested dind без хост-Docker сокета; установка и модель изоляции — +[`WOODPECKER.md`](WOODPECKER.md). ## 8. Резервное копирование diff --git a/deploy/WOODPECKER.md b/deploy/WOODPECKER.md index 34c91a6..533fcb1 100644 --- a/deploy/WOODPECKER.md +++ b/deploy/WOODPECKER.md @@ -1,54 +1,70 @@ -# CI на Gitea + Woodpecker +# CI на Gitea + Woodpecker: изолированный контур -Репозиторий использует **Woodpecker CI 3.x**. Конфигурация лежит в -`.woodpecker/`: +Репозиторий использует **Woodpecker CI 3.x**. Конфигурация — в `.woodpecker/`: | Workflow | Событие | Что делает | |---|---|---| -| `check` | push, pull_request | `ci-preflight.sh`, `test ktlintCheck detekt evidenceCheck` | +| `check` | push, pull_request | тесты, ktlint, detekt, `evidenceCheck` | | `smoke` | push, pull_request | после `check`: полный стек, health, вход админа, один курс | -| `release` | tag, manual | `npm ci`, preflight, полный `ldd-release.sh` (миграции, mutation, load, Chromium/Firefox/WebKit) | +| `release` | tag, manual | миграции, mutation, load, Chromium/Firefox/WebKit E2E, финальный evidence | -Все три workflow используют `concurrency: {limit: 1, group: lms-ci}`: smoke и -release поднимают реальный стек на портах 8080–8090, параллельный запуск занял -бы порты. +## 1. Модель изоляции -## Почему local backend +CI **не должен работать на хосте LMS и не должен иметь доступ к production**. +Разработчики не считаются доверенными: пайплайн может содержать произвольный +код, включая `privileged`-контейнеры. -`check` может работать в Docker-бэкенде, но `smoke`/`release` запускают -`docker compose` и Java-процессы на `localhost`, поэтому им нужен доступ к -Docker и портам хоста. Проще всего один агент с **local backend** на том же -сервере, где стоит Docker. +Схема: -> **Безопасность.** Local backend выполняет пайплайн прямо на хосте без -> изоляции. Он допустим только для **приватного** репозитория с доверенными -> авторами. Не включайте публичные форки/PR от недоверенных пользователей на -> этом агенте: код пайплайна сможет прочитать окружение агента. Если появится -> публичный контур — вынесите `check` на отдельный Docker-backend агент, а -> smoke/release оставьте на изолированной машине. - -## 1. Требования к серверу агента - -- Java 21 в `PATH` (не Java 25 — см. README). -- Docker Engine + Compose v2, пользователь агента в группе `docker`. -- Python 3, bash, git, curl, `ss` (`iproute2`), Node.js 22 + npm. -- Системные библиотеки для Chromium/Firefox/WebKit. На Fedora проще один раз - выполнить `sudo npx playwright install-deps`; WebKit при их отсутствии - автоматически запускается в Docker-образе Playwright. -- Свободные порты 8080–8090 на время smoke/release. -- Достаточно места: Gradle-кэш, Docker-образы, Playwright-браузеры. - -Проверка окружения: - -```bash -./scripts/ci-preflight.sh # для smoke/check -./scripts/ci-preflight.sh --release +``` +Gitea + Woodpecker server отдельная CI VM +┌───────────────────────┐ ┌──────────────────────────────┐ +│ Gitea (production) │ webhook │ Woodpecker agent (Docker) │ +│ Woodpecker server │◄────────►│ └─ pipeline step │ +│ │ gRPC │ └─ nested dockerd (dind)│ +└───────────────────────┘ │ ├─ postgres │ + │ ├─ LMS-сервисы │ + │ └─ Playwright │ + └──────────────────────────────┘ + нет prod-сети и секретов ``` -## 2. Woodpecker server +Правила: -Удобно запустить server в Docker рядом с Gitea. Пример -`docker-compose.woodpecker.yml` (не коммитьте секреты): +- **Отдельная 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: @@ -84,81 +100,105 @@ volumes: [webhook] ALLOWED_HOST_LIST = external,loopback ``` - Также проверьте, что Gitea отдаёт API-страницы не меньше 50 элементов - (значение по умолчанию подходит). + Проверьте, что Gitea отдаёт API-страницы не меньше 50 элементов. 4. Поднимите server и зайдите в UI через Gitea-аккаунт. 5. Включите репозиторий LMS в Woodpecker (Settings → Repositories). +6. В настройках репозитория включите **Trusted** (нужно для `privileged` и + volumes). Доверять можно только этому приватному репозиторию. -## 3. Агент на хосте +## 4. Агент на отдельной VM -Server в контейнере, агент — **на хосте**, иначе local backend не увидит -Java/Docker/Node хоста. +Агент — в Docker, backend `docker`, с доступом к Docker daemon **этой VM**. +Шаги пайплайна получают не хостовый сокет, а собственный nested dockerd. -```bash -sudo useradd --system --create-home --shell /bin/bash woodpecker -sudo usermod -aG docker woodpecker -sudo install -m 0755 woodpecker-agent /usr/local/bin/woodpecker-agent -``` +`docker-compose.agent.yml` на CI VM: -`woodpecker-agent` скачивается из релизов Woodpecker (Linux amd64): -`https://github.com/woodpecker-ci/woodpecker/releases`. - -`/etc/woodpecker-agent.env` (права `0600`, владелец `root`): - -```dotenv -WOODPECKER_SERVER=localhost:9000 -WOODPECKER_AGENT_SECRET=тот-же-секрет-что-у-server -WOODPECKER_BACKEND=local -WOODPECKER_MAX_WORKFLOWS=1 -# Кэш Gradle и браузеров Playwright переживает запуски. Допустимо только для -# приватного репозитория с доверенными авторами (см. предупреждение выше). -WOODPECKER_BACKEND_LOCAL_ISOLATED_HOME=false -``` - -`/etc/systemd/system/woodpecker-agent.service`: - -```ini -[Unit] -Description=Woodpecker CI agent (LMS) -After=network-online.target docker.service -Wants=network-online.target - -[Service] -User=woodpecker -EnvironmentFile=/etc/woodpecker-agent.env -ExecStart=/usr/local/bin/woodpecker-agent -Restart=always -RestartSec=5 - -[Install] -WantedBy=multi-user.target +```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 -sudo systemctl daemon-reload -sudo systemctl enable --now woodpecker-agent -journalctl -u woodpecker-agent -f +docker compose -f docker-compose.agent.yml up -d +docker compose -f docker-compose.agent.yml logs -f ``` -В UI агент появится через несколько секунд. Репозиторий должен быть помечен -как trusted, если вы захотите использовать volumes/privileged (текущему -конфигу они не нужны). +Дополнительно в UI откройте **Settings → Agents** и задайте агенту фильтр +`isolation=dedicated-vm` — это серверная проверка, которую агент не может +подделать. -## 4. Первый прогон +**Общего кэша между пайплайнами нет.** Каждый шаг получает чистый контейнер, +а вложенный Docker storage живёт только внутри него. Это важно: разработчик не +может оставить артефакт, который повлияет на следующий pipeline или релиз. -1. Push в ветку или MR → pipeline `check` → `smoke`. -2. Убедиться, что зелёные оба workflow. -3. Проверить ручной запуск: UI → Run pipeline → event `manual` → workflow +Пайплайны используют предварительно прогретый образ `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` → workflow - `release`; полный отчёт печатается в лог, файлы остаются в - `build/evidence/` рабочей копии агента. +4. Для релиза: `git tag vX.Y.Z && git push origin vX.Y.Z` → `release`. +5. После релиза/по расписанию пересоздайте VM из чистого снапшота. Если pipeline не появляется: -- проверьте, что репозиторий включён в Woodpecker; -- проверьте `WOODPECKER_SERVER` и общий `WOODPECKER_AGENT_SECRET`; -- проверьте, что агент видит Docker (`docker compose version` от пользователя - `woodpecker`) и Java 21; -- проверьте, что путь конфигурации в настройках проекта — `.woodpecker/` - (по умолчанию). +- репозиторий включён в 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. diff --git a/docs/16-stage6-report.md b/docs/16-stage6-report.md index c104c4d..45ba95d 100644 --- a/docs/16-stage6-report.md +++ b/docs/16-stage6-report.md @@ -190,8 +190,9 @@ legacy-активов на production, сдачи по выбранной лаб push/MR выполняет workflow `check` (`test ktlintCheck detekt evidenceCheck`) и `smoke` (`scripts/ldd-smoke.sh`: полный стек, health, вход админа, один курс). Полный `release` обязателен для тегов и запускается вручную для - веток. Агент — local backend с Java 21, Docker Compose v2, Python 3, Node и - Chromium/Firefox/WebKit; установка — [`deploy/WOODPECKER.md`](../deploy/WOODPECKER.md). + веток. Агент — Docker backend на отдельной VM, шаги в nested dind; Java 21, + Docker Compose v2, Python 3, Node и Chromium/Firefox/WebKit; установка — + [`deploy/WOODPECKER.md`](../deploy/WOODPECKER.md). - [ ] Заданы сроки сессии: `LMS_ACCESS_TOKEN_TTL=15m`, `LMS_REFRESH_TOKEN_TTL=7d`, `LMS_SESSION_TIMEOUT=7d`, `LMS_SESSION_ABSOLUTE_TIMEOUT=30d`, `LMS_REFRESH_TOKEN_RETENTION=30d` @@ -323,10 +324,10 @@ legacy-активов на production, сдачи по выбранной лаб 1. **Прогнать инвентаризацию legacy-активов на production/staging** через `/admin/legacy-assets` или `scripts/legacy-assets.sh`, привязать нужные файлы к статьям, остальные удалить. -2. **Развернуть Woodpecker и проверить CI.** Поднять server и local-backend - агент по [`deploy/WOODPECKER.md`](../deploy/WOODPECKER.md), включить - репозиторий, дождаться зелёных `check`/`smoke` на push-MR и `release` на - теге. +2. **Развернуть Woodpecker и проверить CI.** Поднять server и Docker-backend + агент на отдельной VM по [`deploy/WOODPECKER.md`](../deploy/WOODPECKER.md), + включить репозиторий, дождаться зелёных `check`/`smoke` на push-MR и + `release` на теге. 3. **Дальнейшее расширение E2E** — визуальная регрессия и остальные мобильные страницы, если UI продолжит расти. 4. **Пакетные сдачи ведомости**, если появятся курсы, где открытие ведомости diff --git a/docs/17-handoff.md b/docs/17-handoff.md index 32f79c8..803d39f 100644 --- a/docs/17-handoff.md +++ b/docs/17-handoff.md @@ -1,9 +1,9 @@ # 17. Handoff: состояние проекта после закрытия продуктовых рисков **Дата:** 2026-10-28. -**Ветка:** `feature/groups-courses` (последняя история — `git log --oneline`). -**HEAD до переделки CI:** `d98c3b0` (`feat: закрыть остаточный backlog`); -история CI-правки — следующий коммит в `git log`. +**Ветка:** `feature/groups-courses`. +**История:** `git log --oneline`; последние изменения — Woodpecker CI и +изоляция тестовой среды на отдельной VM. **Пуш:** не выполнялся. **Рабочее дерево:** чистое. `exports/` и `texput.log` игнорируются и в коммиты не входят. @@ -129,6 +129,8 @@ вход админа, один курс; - `release` на тег или вручную: тесты, миграции, mutation, runtime/load, browser E2E, финальный evidence. + - workflows требуют метку `isolation=dedicated-vm`; шаги идут в nested dind + без хост-Docker сокета, агент — на отдельной VM без production-доступа. - `scripts/ci-preflight.sh` проверяет Java 21, Docker Compose v2, Python 3, Node и Playwright. Установка server/agent — `deploy/WOODPECKER.md`. - Первый реальный Woodpecker pipeline ещё не подтверждён. @@ -211,11 +213,10 @@ cat build/reports/evidence/index.md у обычных пользователей. 2. **Первый реальный CI-прогон.** - Развернуть Woodpecker server и local-backend агент по + Развернуть Woodpecker server и Docker-backend агент на отдельной VM по `deploy/WOODPECKER.md`, включить репозиторий в Woodpecker и дождаться - зелёных workflow `check`/`smoke` на push-MR и `release` на теге. Агенту - нужны Java 21, Docker Compose v2, Python 3, Node и браузеры - Chromium/Firefox/WebKit. + зелёных workflow `check`/`smoke` на push-MR и `release` на теге. Шаги идут + в nested dind; VM не имеет доступа к production и production-секретам. 3. **Визуальная регрессия и мобильный набор E2E.** Сейчас 6 сценариев × 3 браузера, без screenshot-diff. Добавлять по мере @@ -299,7 +300,9 @@ cat build/reports/evidence/index.md `SessionCookieRefreshFilter.kt`. - `web/.../admin/AdminAuditController.kt`, `LegacyAssetsController.kt`. - `services/gateway/.../JwtAuthenticationFilter.kt`. -- `scripts/ldd-*.sh`, `scripts/ci-preflight.sh`, `scripts/legacy-assets.sh`. +- `scripts/ldd-*.sh`, `scripts/ci-preflight.sh`, `scripts/ci-dind.sh`, + `scripts/ci-build-image.sh`, `scripts/legacy-assets.sh`. - `.woodpecker/{check,smoke,release}.yaml` — CI на Gitea + Woodpecker. +- `ci/Dockerfile` — прогретый образ `lms-ci:1` для изолированного dind. - `deploy/backup.sh`, `deploy/deploy.sh`, `deploy/DEPLOY.md`, `deploy/WOODPECKER.md`. diff --git a/docs/evidence/baseline-2026-10-27.md b/docs/evidence/baseline-2026-10-27.md index ae7b1f0..b4c6758 100644 --- a/docs/evidence/baseline-2026-10-27.md +++ b/docs/evidence/baseline-2026-10-27.md @@ -240,6 +240,8 @@ mutation, runtime + scale и browser E2E, затем финальный `evidenc на каждом push/MR, после `check`; - `release` — полный `ldd-release.sh` с Chromium/Firefox/WebKit и Node: обязателен для тегов, для веток запускается вручную. +- workflows требуют метку `isolation=dedicated-vm`; агент работает на + отдельной VM, шаги — в `lms-ci:1` с nested dockerd и без хост-Docker сокета. - smoke/release начинаются с `scripts/ci-preflight.sh`, который проверяет Java 21, Docker Compose v2, Python 3, Node и Playwright. - Первый реальный прогон на Woodpecker-агенте ещё не подтверждён; локальные @@ -293,8 +295,8 @@ optional json fields`. 6. **Инвентаризация legacy-активов на production не выполнена.** UI, API и скрипт готовы, но реальные файлы нужно привязать или удалить вручную. 7. **Первый CI-прогон не подтверждён.** Конфигурация `.woodpecker/` и - preflight готовы, но Woodpecker-сервер и local-backend агент ещё нужно - развернуть и проверить на первом push-MR и теге. + preflight готовы, но Woodpecker-сервер и Docker-backend агент на отдельной + VM ещё нужно развернуть и проверить на первом push-MR и теге. 8. **Полный release не на каждом коммите.** На push/MR идёт smoke, а mutation/browser/scale — для тегов или вручную. 9. **Интеграция «Контингент» исключена из плана** — без нового ТЗ и доступа к diff --git a/scripts/ci-build-image.sh b/scripts/ci-build-image.sh new file mode 100755 index 0000000..8edd2fe --- /dev/null +++ b/scripts/ci-build-image.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Собирает предварительно прогретый CI-образ на CI VM. +# Образ immutable: исходники пайплайна берутся из checkout, кэш Gradle — из образа. +set -euo pipefail +cd "$(dirname "$0")/.." +tag="${1:-lms-ci:1}" +docker build -f ci/Dockerfile -t "$tag" . +echo "Готово: $tag" +echo "Образ используется в .woodpecker/*.yaml как image: $tag" diff --git a/scripts/ci-dind.sh b/scripts/ci-dind.sh new file mode 100755 index 0000000..b1d0457 --- /dev/null +++ b/scripts/ci-dind.sh @@ -0,0 +1,47 @@ +#!/bin/sh +# Подготовка изолированного Docker-in-Docker шага Woodpecker. +# Запускается внутри privileged-контейнера docker:*-dind: поднимает локальный +# dockerd и доустанавливает Java/Python/bash, если их нет в образе. +# Хост-Docker сокет не используется. +set -eu + +if ! command -v dockerd >/dev/null 2>&1; then + echo "ci-dind.sh: нужен образ docker:-dind" >&2 + exit 1 +fi + +export DOCKER_HOST="${DOCKER_HOST:-unix:///var/run/docker.sock}" +if ! docker info >/dev/null 2>&1; then + echo ">> Запускаю изолированный dockerd..." + dockerd-entrypoint.sh >/tmp/ci-dockerd.log 2>&1 & + i=0 + while [ "$i" -lt 90 ]; do + docker info >/dev/null 2>&1 && break + sleep 1 + i=$((i + 1)) + done + if ! docker info >/dev/null 2>&1; then + echo "dockerd не поднялся:" >&2 + tail -50 /tmp/ci-dockerd.log >&2 || true + exit 1 + fi +fi + +missing="" +command -v bash >/dev/null 2>&1 || missing="$missing bash" +command -v java >/dev/null 2>&1 || missing="$missing openjdk21-jdk" +command -v python3 >/dev/null 2>&1 || missing="$missing python3" +command -v node >/dev/null 2>&1 || missing="$missing nodejs" +command -v npm >/dev/null 2>&1 || missing="$missing npm" +command -v git >/dev/null 2>&1 || missing="$missing git" +command -v curl >/dev/null 2>&1 || missing="$missing curl" +command -v ss >/dev/null 2>&1 || missing="$missing iproute2" + +if [ -n "$missing" ]; then + echo ">> Устанавливаю в изолированный шаг:$missing" + # shellcheck disable=SC2086 + apk add --no-cache $missing +fi + +java -version 2>&1 | head -n 1 +docker info --format 'Docker server {{.ServerVersion}} (изолированный dind)' diff --git a/scripts/ci-preflight.sh b/scripts/ci-preflight.sh index da79cdf..c5b7522 100755 --- a/scripts/ci-preflight.sh +++ b/scripts/ci-preflight.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Проверка окружения Woodpecker-агента (local backend) перед check/smoke/release. +# Проверка окружения изолированного dind-шага Woodpecker перед smoke/release. # Ничего не устанавливает. set -euo pipefail diff --git a/scripts/ldd-browser.sh b/scripts/ldd-browser.sh index 9991510..6dfb233 100755 --- a/scripts/ldd-browser.sh +++ b/scripts/ldd-browser.sh @@ -112,7 +112,7 @@ mkdir -p build/evidence ensure_playwright_browsers set +e if host_playwright_usable; then - log "Запускаю Playwright на хосте (Chromium/Firefox/WebKit)..." + log "Запускаю Playwright в текущем окружении (Chromium/Firefox/WebKit)..." ( cd e2e LDD_WEB_URL="http://localhost:8090" \ @@ -123,10 +123,10 @@ if host_playwright_usable; then else version=$(playwright_version) if [[ -z "$version" ]]; then - echo "WebKit не запускается на хосте, а версию Playwright определить не удалось." >&2 + echo "WebKit не запускается в текущем окружении, а версию Playwright определить не удалось." >&2 E2E_STATUS=1 else - log "WebKit не запускается на хосте; запускаю Chromium/Firefox/WebKit в Docker-образе Playwright ${version}." + log "WebKit не запускается в текущем окружении; запускаю Chromium/Firefox/WebKit в Docker-образе Playwright ${version}." docker run --rm --network host \ -v "$PWD:/work" \ -w /work/e2e \