diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..a4bf52c --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,325 @@ +name: ci + +# CI-пайплайн с гейтами (issue #17, Refs #15) +# lint -> unit (coverage >= 70%) -> security (bandit/pip-audit) -> build -> image-scan (trivy) -> push staging- +# Гейты (роняют pipeline): +# * unit — покрытие < 70 % (--cov-fail-under=70) +# * security — bandit HIGH, pip-audit: любая известная уязвимость +# * image-scan — trivy: HIGH/CRITICAL в образе +# * build стартует только после зелёных lint+unit+security; push — только после зелёного image-scan. + +on: + push: + branches: ["feat/**", "fix/**", "chore/**", "ci/**", "main"] + pull_request: + branches: [main] + workflow_dispatch: + +env: + PYTHON_VERSION: "3.12" + REGISTRY: 192.168.1.111:3000 + IMAGE_NAME: ${{ github.repository }} + +jobs: + # ─────────────────────────────── 1. LINT ──────────────────────────────── + lint: + name: lint + runs-on: ubuntu-latest + outputs: + has_dockerfile: ${{ steps.dockerfile.outputs.has_dockerfile }} + steps: + - name: checkout + id: checkout + uses: actions/checkout@v4 + continue-on-error: true + + # Дефект Gitea git-http: upload-pack отдаёт пустой pack (Content-Length: 90), + # git clone/fetch падает "early EOF / invalid index-pack output". + # Обход: эндпоинт archive/.tar.gz работает — забираем исходники им. + # Когда git-http починят, шаг просто перестанет срабатывать (outcome != failure). + - name: "checkout fallback (archive tarball)" + if: steps.checkout.outcome == 'failure' + run: | + set -euo pipefail + 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 . + echo "sources: archive fallback, $(ls -1 | wc -l) entries" + + - name: "python 3.12" + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt -r requirements-contract.txt + + - name: ruff check + run: ruff check backend + + - name: ruff format --check + run: ruff format --check backend + + - name: "mypy (strict)" + run: mypy backend + + - name: detect deploy/Dockerfile + id: dockerfile + run: | + if [ -f deploy/Dockerfile ]; then + echo "has_dockerfile=true" >> "$GITHUB_OUTPUT" + echo "deploy/Dockerfile найден — jobs build/image-scan/push активны" + else + echo "has_dockerfile=false" >> "$GITHUB_OUTPUT" + echo "::warning::deploy/Dockerfile отсутствует — build/image-scan/push пропущены (ждут merge #18 / PR #49)" + fi + + # ─────────────────── 2. UNIT + ГЕЙТ ПОКРЫТИЯ >= 70 % ─────────────────── + unit: + name: unit + needs: lint + runs-on: ubuntu-latest + steps: + - name: checkout + id: checkout + uses: actions/checkout@v4 + continue-on-error: true + + - name: "checkout fallback (archive tarball)" + if: steps.checkout.outcome == 'failure' + run: | + set -euo pipefail + 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 . + + - name: "python 3.12" + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt -r requirements-contract.txt + + - name: "pytest + coverage gate (fail below 70%)" + run: | + python -m pytest \ + --cov=awg_agent --cov=awg_api \ + --cov-report=term-missing \ + --cov-report=xml:coverage.xml \ + --cov-report=html:htmlcov \ + --cov-fail-under=70 + + - name: coverage summary + if: always() + run: python -m coverage report --sort=cover + + - name: upload coverage artifacts + if: always() + uses: actions/upload-artifact@v3 + with: + name: coverage + path: | + coverage.xml + htmlcov/ + retention-days: 14 + + # ───────────── 3. SECURITY: bandit (HIGH) + pip-audit ────────────────── + security: + name: security + needs: lint + runs-on: ubuntu-latest + steps: + - name: checkout + id: checkout + uses: actions/checkout@v4 + continue-on-error: true + + - name: "checkout fallback (archive tarball)" + if: steps.checkout.outcome == 'failure' + run: | + set -euo pipefail + 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 . + + - name: "python 3.12" + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: install scanners + run: | + python -m pip install --upgrade pip + pip install "bandit[toml]>=1.7" "pip-audit>=2.7" + pip install -r requirements.txt -r requirements-contract.txt + + - name: "bandit -lll (HIGH severity gate)" + run: | + set -o pipefail + bandit -lll -r backend 2>&1 | tee bandit-report.txt + bandit -lll -f json -o bandit-report.json -r backend || true + echo "bandit: HIGH/CRITICAL не найдены" + + - name: "pip-audit (known vulnerabilities gate)" + run: | + pip-audit -r requirements.txt -r requirements-contract.txt --progress-spinner off + + - name: pip-audit json report + if: always() + run: pip-audit -r requirements.txt -r requirements-contract.txt --progress-spinner off --format json -o pip-audit-report.json || true + + - name: upload security artifacts + if: always() + uses: actions/upload-artifact@v3 + with: + name: security-reports + path: | + bandit-report.txt + bandit-report.json + pip-audit-report.json + retention-days: 14 + + # ───────────────── 4. BUILD: docker build (нужен #18) ────────────────── + build: + name: build + needs: [lint, unit, security] + if: needs.lint.outputs.has_dockerfile == 'true' + runs-on: ubuntu-latest + steps: + - name: checkout + id: checkout + uses: actions/checkout@v4 + continue-on-error: true + + - name: "checkout fallback (archive tarball)" + if: steps.checkout.outcome == 'failure' + run: | + set -euo pipefail + 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 . + + - name: "guard: deploy/Dockerfile" + run: | + if [ ! -f deploy/Dockerfile ]; then + echo "::error::deploy/Dockerfile отсутствует: сборка образа ждёт merge #18 (PR #49)." + echo "Пайплайн целиком проверен на ветке feat/17-verify (она же + deploy/Dockerfile)." + exit 1 + fi + + - name: image meta + id: meta + run: | + SHA_SHORT="${GITHUB_SHA::8}" + IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${SHA_SHORT}" + echo "sha_short=${SHA_SHORT}" >> "$GITHUB_OUTPUT" + echo "image=${IMAGE}" >> "$GITHUB_OUTPUT" + echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV" + echo "image: ${IMAGE}" + + - name: docker build + run: | + set -o pipefail + docker build --pull -f deploy/Dockerfile -t "$IMAGE" . 2>&1 | tee build.log + + - name: image digest + run: | + docker image inspect "$IMAGE" --format 'id={{.Id}} size={{.Size}}' + docker image inspect "$IMAGE" --format '{{.Id}}' > image-id.txt + + - name: upload build artifacts + if: always() + uses: actions/upload-artifact@v3 + with: + name: "build-${{ github.sha }}" + path: | + build.log + image-id.txt + retention-days: 14 + + # ──────── 5. IMAGE-SCAN: trivy, гейт HIGH/CRITICAL ──────────────────── + image-scan: + name: image-scan + needs: [lint, build] + if: needs.lint.outputs.has_dockerfile == 'true' + runs-on: ubuntu-latest + env: + TRIVY_VERSION: "0.76.1" + TRIVY_SEVERITY: "HIGH,CRITICAL" + steps: + # trivy запускается официальным образом через docker-сокет: не требует + # скачивания бинаря (github releases отдаёт медленно) + БД кэшируется в + # /opt/trivy-cache на build-хосте. + - name: "trivy image (HIGH/CRITICAL -> fail)" + run: | + set -o pipefail + IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${GITHUB_SHA::8}" + echo "scan: ${IMAGE}" + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /opt/trivy-cache:/root/.cache/trivy \ + "aquasec/trivy:${TRIVY_VERSION}" image \ + --image-src docker --scanners vuln \ + --severity "$TRIVY_SEVERITY" --ignore-unfixed \ + --exit-code 1 --format table "$IMAGE" 2>&1 | tee trivy-report.txt + + - name: trivy json report + if: always() + run: | + set -o pipefail + IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${GITHUB_SHA::8}" + mkdir -p reports + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /opt/trivy-cache:/root/.cache/trivy \ + -v "$PWD/reports:/reports" \ + "aquasec/trivy:${TRIVY_VERSION}" image \ + --image-src docker --scanners vuln \ + --severity "$TRIVY_SEVERITY" --ignore-unfixed \ + --format json -o /reports/trivy-report.json "$IMAGE" || true + cp reports/trivy-report.json ./trivy-report.json 2>/dev/null || true + + - name: upload trivy report + if: always() + uses: actions/upload-artifact@v3 + with: + name: trivy-report + path: | + trivy-report.txt + trivy-report.json + retention-days: 14 + + # ─────── 6. PUSH: staging- в registry Gitea ────────────────────── + push: + name: push + needs: [lint, build, image-scan] + if: needs.lint.outputs.has_dockerfile == 'true' && github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - name: docker login + run: | + set -o pipefail + # Приоритет — секрет с правом write:package (REGISTRY_TOKEN), иначе встроенный токен Actions. + TOKEN="${{ secrets.REGISTRY_TOKEN }}" + if [ -z "$TOKEN" ]; then + TOKEN="${{ secrets.GITHUB_TOKEN }}" + fi + echo "$TOKEN" | docker login "$REGISTRY" -u "${{ github.actor }}" --password-stdin + + - name: "push staging-" + run: | + set -o pipefail + IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${GITHUB_SHA::8}" + docker push "$IMAGE" 2>&1 | tee push.log + echo "${IMAGE}" | tee pushed-image.txt + + - name: upload push record + if: always() + uses: actions/upload-artifact@v3 + with: + name: pushed-image + path: | + push.log + pushed-image.txt + retention-days: 14 diff --git a/docs/ci/CI.md b/docs/ci/CI.md new file mode 100644 index 0000000..e82aa13 --- /dev/null +++ b/docs/ci/CI.md @@ -0,0 +1,199 @@ +# 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 /:staging-` | ошибка сборки | `build.log`, `image-id.txt` | +| `image-scan` | `docker run aquasec/trivy: image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1` (по сокету) | **HIGH/CRITICAL в образе** | `trivy-report.txt`, `trivy-report.json` | +| `push` | `docker login` + `docker push ...:staging-` | ошибка авторизации/пуша | `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//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//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//rerun +``` + +## 5. Registry (push staging-) + +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/.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-контейнер остановлен. + +### 6.4 Артефакты Actions не публикуются + +`actions/upload-artifact@v3` в логе рапортует `Artifact has been successfully uploaded!`, +но `GET /actions/runs//artifacts` → `{"artifacts":[],"total_count":0}` и в UI пусто; +`@v4` зависает на старте экшена (>6 мин). Платформенный дефект Gitea 1.26.0 + act_runner v0.6.1 — issue #62. + +**Мера в пайплайне:** `unit` печатает coverage-таблицу (`coverage report --sort=cover`), `security` — вывод +`bandit`/`pip-audit`, `image-scan` — trivy-таблицу. Эти строки в логе job'а и есть доступный +артефакт: ссылка на job лежит в commit status. Скачать `coverage.xml`/`htmlcov`/`trivy-report.json` +из UI нельзя, пока #62 не закрыт. + +### 6.5 `contract-openapi.yml` — тот же дефект git-http + +Оба job'а (`openapi-contract`, `ts-client-is-current`) падали на шаге `checkout` за 28–50 с +(`bad pack header`, exit 128) и делали combined status любого PR = `failure`, хотя `ci / *` зелёные. +В ветке `fix/59-checkout-fallback` (PR #63) применён тот же archive-fallback; `ts-client-is-current` +после fallback сравнивает `sha256sum` перегенерированного `ui/src/api/generated/schema.d.ts` +вместо `git diff` (в распакованном архиве нет `.git`). Проверено: run #64 — success. + +## 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 +``` diff --git a/docs/ci/RUNNER-CHECK-17-v2.md b/docs/ci/RUNNER-CHECK-17-v2.md new file mode 100644 index 0000000..eee7267 --- /dev/null +++ b/docs/ci/RUNNER-CHECK-17-v2.md @@ -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/.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-` | ошибка сборки | +| image-scan | `trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1` | **HIGH/CRITICAL в образе** | +| push | `docker login` + `docker push ...:staging-` | ошибка авторизации/пуша | + +`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-` собран на 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//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//rerun`. +- Ручной запуск: `workflow_dispatch` у `ci.yml`. +- Логи job'а: `GET /api/v1/repos/embeddevio/awg-dash/actions/jobs//logs`. +- Полный runbook: `docs/ci/CI.md` в ветке `feat/17-ci`.