docs(ci): runbook пайплайна и отчёт проверки runner'а (#17)
ci / lint (push) Successful in 42s
contract-openapi / openapi-contract (push) Failing after 48s
contract-openapi / ts-client-is-current (push) Failing after 50s
ci / lint (pull_request) Successful in 42s
contract-openapi / openapi-contract (pull_request) Failing after 40s
contract-openapi / ts-client-is-current (pull_request) Failing after 28s
ci / unit (push) Successful in 49s
ci / security (push) Successful in 3m10s
ci / unit (pull_request) Successful in 48s
ci / security (pull_request) Successful in 2m22s
ci / build (push) Has been skipped
ci / image-scan (push) Has been skipped
ci / build (pull_request) Has been skipped
ci / push (push) Has been skipped
ci / image-scan (pull_request) Has been skipped
ci / push (pull_request) Has been skipped

Refs #17, Refs #15
This commit is contained in:
2026-10-28 23:12:17 +03:00
parent aac82e6d23
commit bbe0886342
2 changed files with 318 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
# CI/CD: пайплайн с гейтами (`ci.yml`)
**Владелец:** devops · **Issue:** #17 (Refs #15) · **Файл:** `.gitea/workflows/ci.yml`
**Runner:** `awg-dash-147-repo` (repo-scoped) на Docker-хосте `192.168.1.147`
---
## 1. Триггеры
| Событие | Ветки | Что запускается |
|---|---|---|
| `push` | `feat/**`, `fix/**`, `chore/**`, `ci/**`, `main` | полный пайплайн |
| `pull_request` (sync) | базис `main` | полный пайплайн |
| `workflow_dispatch` | любая | полный пайплайн (ручной перезапуск) |
---
## 2. Стадии и гейты
```
lint ──┬── unit ────────┐
│ ├── build ── image-scan ── push (только push-событие)
└── security ────┘
```
| Job | Что делает | Гейт (роняет pipeline) | Артефакты |
|---|---|---|---|
| `lint` | `ruff check backend`, `ruff format --check backend`, `mypy backend` (strict) | любая ошибка линтера/типов | — |
| `unit` | `pytest --cov=awg_agent --cov=awg_api --cov-fail-under=70` | **покрытие < 70 %** | `coverage.xml`, `htmlcov/` |
| `security` | `bandit -lll -r backend`, `pip-audit -r requirements.txt` | **bandit severity HIGH**, любая известная уязвимость в зависимостях | `bandit-report.txt/.json`, `pip-audit-report.json` |
| `build` | `docker build -f deploy/Dockerfile -t <registry>/<repo>:staging-<sha8>` | ошибка сборки | `build.log`, `image-id.txt` |
| `image-scan` | `docker run aquasec/trivy:<ver> image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1` (по сокету) | **HIGH/CRITICAL в образе** | `trivy-report.txt`, `trivy-report.json` |
| `push` | `docker login` + `docker push ...:staging-<sha8>` | ошибка авторизации/пуша | `push.log`, `pushed-image.txt` |
**trivy** запускается официальным образом через docker-сокет (бинарь с github releases
в homelab качается крайне медленно), БД кэшируется в `/opt/trivy-cache` на build-хосте.
**Токен для push**: приоритет — секрет `REGISTRY_TOKEN` (PAT с правом `write:package`),
fallback — встроенный `secrets.GITHUB_TOKEN`. Проверено: PAT без `write:package` получает
`401 Unauthorized` на `POST /v2/<repo>/blobs/uploads/` — если push падает с 401, заведите секрет.
**Порядок гарантий:**
- `build` стартует только после зелёных `lint` + `unit` + `security`.
- `image-scan` — только после `build`; `push` — только после зелёного `image-scan`.
- Тег образа: `staging-<первые 8 символов GITHUB_SHA>` в registry `192.168.1.111:3000/embeddevio/awg-dash`.
## 3. Условная активация build/image-scan/push
`build`/`image-scan`/`push` выполняются, только если в репозитории есть `deploy/Dockerfile`
(вычисляется в `lint` → output `has_dockerfile`). До merge #18 (PR #49) они **скипаются**
с предупреждением в логе — это ожидаемое поведение, а не сбой.
## 4. Runner
| Параметр | Значение |
|---|---|
| Контейнер | `gitea-runner-awg-repo` (`docker.io/gitea/act_runner:latest`, v0.6.1) на `192.168.1.147` |
| Область | **repo-scoped** (зарегистрирован repo-runner-token'ом для `embeddevio/awg-dash`) |
| Label | `ubuntu-latest` → образ `catthehacker/ubuntu:act-22.04` |
| Параллелизм | `capacity: 1`, `maxParallel=1` |
| Сеть job-контейнеров | `container.network: "host"` (см. п. 7 — иначе исчерпывается пул адресов Docker) |
| Docker socket | проброшен (`/var/run/docker.sock`), `container.options: ""` — v0.6.1 монтирует сокет сам, дубль в `options` даёт `Duplicate mount point` |
| Кэш инструментов | volume `act-toolcache` → `/opt/hostedtoolcache` (Python 3.12 скачивается один раз) |
| Конфиг | `/opt/gitea-runner-awg-repo/config.yaml`, состояние — `/opt/gitea-runner-awg-repo/.runner` |
### Диагностика runner'а
```bash
ssh root@192.168.1.147
docker ps --filter name=gitea-runner # жив ли
docker logs --tail 50 gitea-runner-awg-repo # задачи, ошибки
docker logs gitea-runner-awg-repo 2>&1 | grep -E "task|Error|failed"
```
Лог job'а с его шагами и результатом доступен через API Gitea:
```bash
curl -H "Authorization: token $GITEA_TOKEN" \
http://192.168.1.111:3000/api/v1/repos/embeddevio/awg-dash/actions/jobs/<job_id>/logs
```
Перезапуск конкретного run'а без нового коммита:
```bash
curl -X POST -H "Authorization: token $GITEA_TOKEN" \
http://192.168.1.111:3000/api/v1/repos/embeddevio/awg-dash/actions/runs/<run_id>/rerun
```
## 5. Registry (push staging-<sha>)
Gitea-registry `192.168.1.111:3000` работает по **HTTP**, поэтому Docker-демон на
build-хосте должен знать его как insecure:
```json
// /etc/docker/daemon.json
{ "log-driver": "journald", "insecure-registries": ["192.168.1.111:3000"] }
```
Применяется **без перезапуска контейнеров**:
```bash
systemctl reload docker # SIGHUP, контейнеры продолжают работать
docker info | grep -A3 "Insecure Registries"
```
Авторизация в job'е — `${{ secrets.GITHUB_TOKEN }}` (выдаётся Gitea Actions автоматически), пользователь `${{ github.actor }}`.
## 6. Известные дефекты инфраструктуры
### 6.1 Gitea git-http отдаёт пустой pack → `actions/checkout` не работает
**Симптом:** `git clone`/`git fetch` с любого хоста падает:
```
fatal: early EOF
fatal: fetch-pack: invalid index-pack output
```
**Причина (измерено):** ответ `git-upload-pack` содержит `Content-Length: 90` — только
`shallow-info` + заголовок `packfile`, без самих объектов. То есть обрыв на стороне
Gitea (git-http), а не у клиента. `ls-remote` и эндпоинт `archive/<sha>.tar.gz` при этом
работают (архив 200 OK, валидный tar).
**Обход в `ci.yml`:** основной путь — `actions/checkout@v4` (`continue-on-error: true`);
если он падает, следующий шаг забирает исходники архивом:
```bash
curl -fsSL -o /tmp/src.tar.gz "http://192.168.1.111:3000/${IMAGE_NAME}/archive/${GITHUB_SHA}.tar.gz"
tar -xzf /tmp/src.tar.gz --strip-components=1 -C .
```
Когда git-http починят, fallback перестанет срабатывать сам (условие `outcome == 'failure'`).
Исправление на стороне Gitea-хоста (`192.168.1.111`) — вне доступа агентов (нет SSH).
### 6.2 «all predefined address pools have been fully subnetted»
Каждый job создаёт Docker-сеть `GITEA-ACTIONS-*`; прерванные job'ы оставляли сети,
и пул `172.17.0.0/16`-диапазонов исчерпался → job'ы падали на создании контейнера.
Лечение (безопасно, только сети без контейнеров):
```bash
for n in $(docker network ls --filter name=GITEA-ACTIONS -q); do
[ "$(docker network inspect "$n" --format '{{len .Containers}}')" = "0" ] && docker network rm "$n"
done
```
Профилактика: `container.network: "host"` в конфиге runner'а (job-сети не создаются).
Кардинальное решение — `default-address-pools` в `daemon.json`, но оно требует
**перезапуска** демона (в отличие от `insecure-registries`), поэтому только по согласованию.
### 6.3 Instance-scoped runner не отчитывается о статусах
Runner, зарегистрированный instance-токеном (`gitea-runner-awg`), забирал задачи, но
Gitea не фиксировал результаты: jobs вечно в `queued`, `runner_id: 0`, логов нет.
Repo-scoped runner (`gitea-runner-awg-repo`, id 2) с тем же образом пишет статусы и логи
корректно. Legacy-контейнер остановлен.
## 7. Локальное воспроизведение гейтов
```bash
python3.12 -m venv .venv && . .venv/bin/activate
pip install -r requirements-dev.txt
ruff check backend && ruff format --check backend && mypy backend # lint
pytest # unit + coverage >= 70%
bandit -lll -r backend && pip-audit -r requirements.txt # security
docker build -f deploy/Dockerfile -t awg-dash:local . # build
trivy image --severity HIGH,CRITICAL --exit-code 1 awg-dash:local # image-scan
```
Проверка, что гейты действительно роняют пайплайн:
```bash
# покрытие: временно сузить --cov до одного модуля или добавить непокрытый код
pytest --cov=awg_agent --cov-fail-under=70 # FAIL: покрытие ниже порога
# security: HIGH-находка (eval / shell=True / weak hash)
bandit -lll -r backend # exit 1 при Severity: High
```
+138
View File
@@ -0,0 +1,138 @@
# Отчёт: Gitea Actions runner + CI-пайплайн `embeddevio/awg-dash` (v2)
**Владелец:** devops · **Дата:** 2026-10-28 · **Refs #17, Refs #15**
**Ветка:** `feat/17-ci` · **Ветка проверки:** `feat/17-verify` (= #17 + `deploy/*` из #18)
## 1. Итог
Runner, поднятый 28.10, **фактически не работал**: задачи забирались, но падали на создании
контейнера, а результаты не фиксировались в Gitea. Причины найдены и устранены на build-хосте
`192.168.1.147`. Пайплайн `ci.yml` написан, закоммичен и **прогнан на живом runner'е** —
все три гейта подтверждены реальными падениями jobs.
Осталось три внешних пункта, которые агент закрыть не может (нет SSH к `192.168.1.111`,
нет прав на merge): git-http дефект Gitea (#59), артефакты Gitea не публикуются,
`deploy/Dockerfile` появится в `main` только после merge #18.
## 2. Что было сломано в runner'е и как исправлено
| # | Симптом | Причина | Исправление |
|---|---|---|---|
| 1 | jobs вечно `queued`, `runner_id: 0`, логов нет | runner зарегистрирован **instance-token'ом** (`gitea-runner-awg`): задачи забирались, но результат не привязывался к репозиторию | зарегистрирован **repo-scoped** runner `awg-dash-147-repo` (repo registration token через API), `online`, id 2; legacy-контейнер остановлен |
| 2 | `failed to create container: Duplicate mount point: /var/run/docker.sock` | `container.options` монтировал сокет, а act_runner v0.6.1 монтирует его сам | `options: ""` + `valid_volumes: ["/var/run/docker.sock"]` |
| 3 | `all predefined address pools have been fully subnetted` | 23 «висимых» сети `GITEA-ACTIONS-*` от прерванных job'ов вычерпали пул Docker | удалены все `GITEA-ACTIONS-*` с 0 контейнеров (31 → 8 сетей); `container.network: "host"` как профилактика |
| 4 | `git clone` в шаге checkout: `bad pack header` | дефект Gitea git-http (issue #59), не runner | в `ci.yml` — fallback на `archive/<sha>.tar.gz` (archive работает: 200, валидный tar) |
| 5 | trivy: `install.sh` + `v0.57.1` → 404, tarball с github releases качается минутами | неверная версия в скрипте + медленная отдача github releases | trivy запускается официальным образом `aquasec/trivy:0.76.1` через docker-сокет, БД кэшируется в `/opt/trivy-cache` |
## 3. Итоговая конфигурация runner'а
```
контейнер : gitea-runner-awg-repo (docker.io/gitea/act_runner:latest v0.6.1), 192.168.1.147
область : repo-scoped (embeddevio/awg-dash), runner id 2, online
label : ubuntu-latest -> catthehacker/ubuntu:act-22.04
capacity : 1 (maxParallel=1)
сеть job : host, socket /var/run/docker.sock проброшен
toolcache : volume act-toolcache -> /opt/hostedtoolcache (Python 3.12.16 установлен один раз)
trivy : образ aquasec/trivy:0.76.1 + БД-кэш /opt/trivy-cache
registry : 192.168.1.111:3000 добавлен в insecure-registries на .147
(применено `systemctl reload docker`, контейнеры не перезапускались)
```
## 4. Пайплайн `.gitea/workflows/ci.yml`
```
lint ─┬─ unit ───────┐
│ ├─ build ── image-scan ── push (только push-событие)
└─ security ───┘
```
| Job | Команды | Гейт |
|---|---|---|
| lint | `ruff check backend`, `ruff format --check backend`, `mypy backend` | любая ошибка |
| unit | `pytest --cov=awg_agent --cov=awg_api --cov-report=xml/html --cov-fail-under=70` | **coverage < 70 %** |
| security | `bandit -lll -r backend`, `pip-audit -r requirements.txt -r requirements-contract.txt` | **bandit HIGH**, известные CVE в зависимостях |
| build | `docker build --pull -f deploy/Dockerfile -t 192.168.1.111:3000/embeddevio/awg-dash:staging-<sha8>` | ошибка сборки |
| image-scan | `trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1` | **HIGH/CRITICAL в образе** |
| push | `docker login` + `docker push ...:staging-<sha8>` | ошибка авторизации/пуша |
`build`/`image-scan`/`push` активируются только при наличии `deploy/Dockerfile`
(output `has_dockerfile` из `lint`); до merge #18 они скипаются с предупреждением.
## 5. Прогоны на живом runner'е (доказательства по AC)
### 5.1 `feat/17-ci` (ветка от `main`, Dockerfile ещё нет) — run #39
http://192.168.1.111:3000/embeddevio/awg-dash/actions/runs/39
| Job | Результат | Детали |
|---|---|---|
| lint | ✅ success | ruff check/format + mypy strict |
| unit | ✅ success | **198 passed**, coverage **96.80 %** (порог ≥ 70 %) |
| security | ✅ success | bandit: 0 HIGH, pip-audit: CVE не найдены |
| build / image-scan / push | ⏭ skipped | нет `deploy/Dockerfile` (ждёт #18) — штатное поведение |
### 5.2 `feat/17-verify` (= #17 + `deploy/*` из #18) — run #49
http://192.168.1.111:3000/embeddevio/awg-dash/actions/runs/49
| Job | Результат | Детали |
|---|---|---|
| lint | ✅ success | |
| unit | ✅ success | 198 passed, coverage 96.80 % |
| security | ✅ success | |
| build | ✅ success | образ `192.168.1.111:3000/embeddevio/awg-dash:staging-<sha8>` собран на runner'е |
| image-scan | ❌ failure | `Total: 22 (HIGH: 21, CRITICAL: 1)` — golang stdlib v1.25.14, `golang.org/x/crypto` — см. п. 6.1 |
| push | ⏭ skipped | fail-closed: при красном image-scan образ не публикуется |
### 5.3 Негативные прогоны гейтов — `ci/17-gate-negative` (ветка-пробник, удалена)
| Проверка | Run | Что в логе |
|---|---|---|
| покрытие < порога | #51 http://192.168.1.111:3000/embeddevio/awg-dash/actions/runs/51 | `FAIL Required test coverage of 99% not reached. Total coverage: 96.80%` → job `unit` **failure** |
| HIGH-находка bandit | #55 http://192.168.1.111:3000/embeddevio/awg-dash/actions/runs/55 | `Issue: [B602 ...] Severity: High Confidence: High` → job `security` **failure**, pipeline красный |
| HIGH/CRITICAL в образе | #49 (см. 5.2) | 22 находки → `image-scan` **failure** |
**Вывод по AC:** пуш в `feat/*` даёт jobs `lint`/`unit`/`security`/`build`; снижение покрытия
ниже порога роняет job; HIGH-уязвимость (код или образ) роняет pipeline.
## 6. Открытые вопросы (эскалация)
### 6.1 Образ содержит 21 HIGH + 1 CRITICAL (нужен апгрейд сборки, #18)
Trivy по образу, собранному из `deploy/Dockerfile` (#18):
- `stdlib` Go **v1.25.14** → CVE-2026-56857 (CRITICAL), CVE-2026-111943, CVE-2026-78667, CVE-2026-97031 (HIGH);
- `golang.org/x/crypto v0.42.0` → CVE-2025-47913 (HIGH, fix в 0.43.0).
Причина: `ARG GO_IMAGE=golang:1.25-alpine` в `deploy/Dockerfile`. Гейт работает правильно —
публикация образа заблокирована. Требуется бамп Go до ≥ 1.26.10 и `golang.org/x/crypto` ≥ 0.43.0
(владелец #18), после этого `image-scan` и `push` пойдут зелёными без правок CI.
### 6.2 Артефакты Gitea не публикуются
`actions/upload-artifact@v3` отрабатывает без ошибок («Artifact has been successfully uploaded!»,
173 КБ на coverage), но ни API (`/actions/runs/<id>/artifacts`, `/actions/artifacts`), ни UI
артефактов не показывают (`total_count: 0`). `upload-artifact@v4` **зависает** (job висел >6 мин
на старте экшена, контейнер пришлось снять). Итог: содержимое артефактов в Gitea 1.26 + runner
v0.6.1 не сохраняется — поэтому в шагах дополнительно печатается сводка в лог job
(coverage-таблица, bandit-отчёт, trivy-таблица) — эти логи стабильны и доступны по ссылке run'а.
### 6.3 Git-http Gitea (issue #59)
Checkout штатным `actions/checkout` падает (`bad pack header`), работает только архивный
fallback. Нужен доступ к хосту Gitea `192.168.1.111` (fsck/repack, проверка диска) — у агентов
его нет.
### 6.4 Token для registry push
PAT агента не имеет `write:package` → `401 Unauthorized` на `POST /v2/.../blobs/uploads/`
(проверено вручную на .147). В `ci.yml` логин берёт `secrets.REGISTRY_TOKEN`, если он задан,
иначе `secrets.GITHUB_TOKEN`. Для гарантированного push нужен секрет `REGISTRY_TOKEN`
(owner, PAT с `write:package`).
## 7. Эксплуатация
- Перезапуск run'а без коммита: `POST /api/v1/repos/embeddevio/awg-dash/actions/runs/<id>/rerun`.
- Ручной запуск: `workflow_dispatch` у `ci.yml`.
- Логи job'а: `GET /api/v1/repos/embeddevio/awg-dash/actions/jobs/<job_id>/logs`.
- Полный runbook: `docs/ci/CI.md` в ветке `feat/17-ci`.