docs: описать offline CI (два runner'а, что тестируется, нюансы)
This commit is contained in:
187
docs/CI.md
Normal file
187
docs/CI.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# CI.md — офлайн-автотесты на self-hosted Gitea Actions
|
||||
|
||||
Сервер Gitea изолирован (без постоянного интернета), поэтому здесь нет
|
||||
`actions/checkout`, нет Marketplace-экшенов и нет ни одного `uses:` — только
|
||||
`run:`. Прежде чем править `.gitea/workflows/*`, прочитай это целиком: почти
|
||||
все неожиданные падения объясняются структурой раздела «Нюансы окружения»
|
||||
ниже, а не багом в тесте.
|
||||
|
||||
## Два runner'а — зачем и как разделены
|
||||
|
||||
Сигнальный тракт pluto-link целиком живёт на ARM (Cortex-A9 внутри Pluto), а
|
||||
сам Gitea-сервер — x86_64. Проверить можно только то, что не требует
|
||||
Pluto-железа: (1) чистую логику на libc, (2) факт компиляции под armhf,
|
||||
(3) статические grep-правила. Для этого заведено два независимых runner'а с
|
||||
разными метками — они физически разные Docker-контейнеры на одном сервере:
|
||||
|
||||
| Runner | Контейнер | База образа | Метка | Что гоняет |
|
||||
|---|---|---|---|---|
|
||||
| основной | `gitea_runner` | Alpine `act_runner_tools:0.2.13` | `host` | 01-smoke, 02-guardrails |
|
||||
| cross-builder | `gitea_runner_armhf` | Debian bookworm, **нативный x86-64** | `armhf` | 03-gftest, 04-cross-build |
|
||||
|
||||
У каждого `capacity: 1` в `config.yaml` — внутри одного runner'а job'ы идут
|
||||
строго последовательно (см. «Нюансы» ниже про параллелизм между runner'ами).
|
||||
|
||||
Оба используют host-executor: шаги `run:` выполняются прямо в окружении
|
||||
процесса runner (то есть внутри контейнера), без docker-in-docker. Отсюда то
|
||||
же правило, что и в примерах для других проектов на этом сервере: **что
|
||||
лежит в образе runner'а — то и доступно job'у; что стоит на Debian-хосте
|
||||
сервера — не видно job'у вообще.**
|
||||
|
||||
## Что проверяет каждый workflow
|
||||
|
||||
### `.gitea/workflows/01-smoke.yml` — информационный
|
||||
Печатает `/etc/os-release` и `uname -a` (докажет, что это Alpine job-окружения,
|
||||
а не Debian-хост сервера), какие инструменты есть (`git`/`bash`/`cc`/`make`/
|
||||
`arm-linux-gnueabihf-gcc`) и есть ли `$XARM/lib/*.a`. **Никогда не падает** —
|
||||
только помечает `[ЕСТЬ]`/`[НЕТ]`. Зелёный означает буквально одно: «runner
|
||||
жив и взял задачу», не более. `timeout-minutes: 5`.
|
||||
|
||||
### `.gitea/workflows/02-guardrails.yml` — статический страж
|
||||
Клонирует репозиторий (без `actions/checkout`, см. ниже) и гоняет
|
||||
`scripts/ci_guardrails.sh` — 17 grep/bash-ассертов на жёсткие правила
|
||||
CLAUDE.md: sample rate `1920000` и никогда `3840000`, `header_len=12` на TX
|
||||
**и** RX (техдолг #1), только `local:` в тракте, статические/ARM-флаги в
|
||||
makefile, отсутствие тяжёлых зависимостей, отсутствие Python в `src/`. Скрипт
|
||||
самодостаточен и гоняется точно так же локально:
|
||||
```sh
|
||||
scripts/ci_guardrails.sh
|
||||
```
|
||||
Тулчейн не нужен — работает на базовом Alpine-образе. `timeout-minutes: 5`.
|
||||
|
||||
### `.gitea/workflows/03-gftest.yml` — юнит-тест group-FEC
|
||||
`make gftest` (см. `makefile`): нативная (не кросс-!) сборка
|
||||
`src/group_fec.c` с `-DGF_SELFTEST` через хостовый `cc` cross-builder'а и
|
||||
запуск самопроверки GF(256) — 774 400 кейсов восстановления группового FEC
|
||||
16+2, честный pass/fail по коду возврата. Модуль тянет только `<string.h>/
|
||||
<stdint.h>/<stdio.h>/<stdlib.h>`, поэтому компилируется любым gcc. Идёт на
|
||||
`runs-on: armhf`, потому что там гарантированно есть нативный `cc`
|
||||
(`build-essential`) — это не про ARM, просто удобно совместить с
|
||||
cross-builder'ом. `timeout-minutes: 5`.
|
||||
|
||||
### `.gitea/workflows/04-cross-build.yml` — приёмочный тест ARM-тулчейна
|
||||
`make check-env && make all` — реальная кросс-сборка `transmitter`,
|
||||
`receiver`, `ofdm_bench` под armhf с `-Wall -Wextra -static`. Ловит любую
|
||||
компиляционную регрессию в боевом коде до деплоя на плату. `timeout-minutes: 15`.
|
||||
|
||||
## Ключевой архитектурный момент: это НЕ эмуляция ARM
|
||||
|
||||
Первая попытка поднять второй runner использовала `platform: linux/arm64` /
|
||||
`linux/arm/v7` в compose — и падала с `exec /sbin/tini: exec format error`:
|
||||
Docker пытался выполнить ARM-бинарь на x86-ядре сервера без
|
||||
qemu/binfmt-регистрации.
|
||||
|
||||
Это была неверная модель. **Кросс-компиляция не требует ARM-машины.**
|
||||
`arm-linux-gnueabihf-gcc` — обычный **x86-бинарь**, который на входе берёт
|
||||
`.c` и на выходе даёт ARM-машинный код; выполняется на полной скорости
|
||||
x86-ядра, без единого такта эмуляции. Это ровно то же самое, что уже
|
||||
десятилетиями делает Raspberry Pi-хост в `scripts/build_deps.sh` — только
|
||||
хост там aarch64, а не x86_64, что для кросс-компиляции роли не играет.
|
||||
|
||||
Рабочее решение: `gitea_runner_armhf` — **нативный x86-64** контейнер
|
||||
(`FROM debian:bookworm`, без `platform:` в compose), в который *вложен*
|
||||
ARM-кросс-тулчейн как обычный пакет (`gcc-arm-linux-gnueabihf`). Сам
|
||||
`act_runner` в этом контейнере тоже нативный `linux-amd64` — ARM в цепочке
|
||||
нет нигде, кроме результата компиляции.
|
||||
|
||||
## Нюансы окружения (то, что реально ломалось)
|
||||
|
||||
**musl vs glibc — «файл есть» ≠ «работает».** На Alpine (`host`-runner)
|
||||
glibc-бинарь может лежать на диске и даже отвечать на `command -v`, но
|
||||
падать с обманчивым `not found` при запуске — потому что нет `ld-linux`.
|
||||
Поэтому все приёмочные шаги здесь **компилируют и запускают** пробную
|
||||
программу, а не проверяют `command -v`. В `04-cross-build.yml` это доведено
|
||||
до предела: после сборки пробника парсится байт `e_machine` прямо из
|
||||
ELF-заголовка (`od -An -tx1 -j18 -N2`, ждём `2800` = `EM_ARM` в
|
||||
little-endian) — «собралось без ошибки» тоже не значит «собралось под ту
|
||||
архитектуру». Тот же разбор повторяется для всех трёх собранных бинарников в
|
||||
`04`, а не только для пробника.
|
||||
|
||||
**`$XARM` перенесён с RPi, не пересобирается в CI.** Пять статических
|
||||
armhf-библиотек (`libliquid.a`, `libfec.a`, `libfftw3f.a`, `libiio.a`,
|
||||
`libad9361.a`) — результат `scripts/build_deps.sh` (~15–20 мин, нужен
|
||||
интернет и sudo). Пересобирать их в образе cross-builder'а на каждый билд
|
||||
образа избыточно: они `.a`-файлы с ARM-машинным кодом, архитектура и ОС
|
||||
хоста, на котором их скопировали, роли не играет. Поэтому в
|
||||
`runner-image/Dockerfile` (лежит на сервере, `/opt/gitea-runner-armhf/`, вне
|
||||
этого git-репозитория) готовый `~/xarm` с RPi просто кладётся `COPY xarm
|
||||
/root/xarm` — путь совпадает с дефолтным `XARM ?= $(HOME)/xarm` из
|
||||
`makefile`, поэтому `make check-env`/`make all` в CI работают без
|
||||
дополнительных переменных окружения.
|
||||
|
||||
**Циклические зависимости статических либ потребовали group-linking.**
|
||||
Прямой прогон `make all` на реальном cross-builder'е (не на бумаге) вскрыл,
|
||||
что однопроходного порядка `-lad9361 -liio -lliquid -lfec -lfftw3f` иногда
|
||||
недостаточно — компоновщик требует повторных проходов по кругу
|
||||
`ad9361↔iio`. Исправлено в `makefile` через `-Wl,--start-group ... -Wl,--end-group`
|
||||
вокруг всего списка библиотек (коммит `738fd4a`): линкер пересматривает
|
||||
группу, пока не разрешит все символы, вместо одного прохода слева направо.
|
||||
Это именно то, ради чего 04-cross-build существует — баг был невидим на
|
||||
бумаге и всплыл только на реальной кросс-сборке в CI.
|
||||
|
||||
**Параллелизм: между runner'ами есть, внутри — нет.** `capacity: 1` у
|
||||
каждого runner'а сериализует job'ы **в пределах одного контейнера**, но
|
||||
`host` и `armhf` — независимые процессы с раздельной очередью, поэтому
|
||||
01/02 (на `host`) и 03/04 (на `armhf`) в реальности выполняются
|
||||
параллельно друг другу; наблюдалось по чередованию task-id в логах push'а.
|
||||
Внутри `armhf` 03 и 04 всегда строго последовательны (04 ждёт освобождения
|
||||
runner'а после 03).
|
||||
|
||||
**Клон репозитория — без `actions/checkout`.** Marketplace-экшены
|
||||
недоступны офлайн. Вместо этого каждый job сам клонирует себя через
|
||||
токен job'а:
|
||||
```yaml
|
||||
- name: Получить код репозитория
|
||||
env:
|
||||
REPO_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
rm -rf repo
|
||||
git clone --quiet "http://x-access-token:${REPO_TOKEN}@localhost:3000/${GITHUB_REPOSITORY}.git" repo
|
||||
git -C repo checkout --quiet "$GITHUB_SHA"
|
||||
```
|
||||
`--quiet` обязателен: без него git печатает URL с токеном в progress-вывод,
|
||||
который попадает в открытые логи Actions.
|
||||
|
||||
**Уборка рабочего каталога — `if: always()`.** Оба runner'а — общие
|
||||
контейнеры для всех job'ов; мусор от упавшего шага иначе копится в ФС
|
||||
контейнера до пересоздания.
|
||||
|
||||
**Токен регистрации в логах — разовая гигиена.** `GITEA_RUNNER_REGISTRATION_TOKEN`
|
||||
из `docker-compose.runner-armhf.yml` виден в истории команд/логах настройки.
|
||||
Он однократный (используется только при первой регистрации — `data/.runner`
|
||||
уже существует), но по итогам стоит сбросить в Gitea (Site Administration →
|
||||
Actions → Runners → сброс токена регистрации), чтобы старое значение не
|
||||
годилось для привязки постороннего runner'а к инстансу.
|
||||
|
||||
## Как гонять локально, без runner'а
|
||||
|
||||
```sh
|
||||
scripts/ci_guardrails.sh # 02 — только bash/grep, где угодно
|
||||
make gftest # 03 — нужен любой hostовый cc
|
||||
make check-env && make all # 04 — нужен arm-linux-gnueabihf-gcc + $XARM
|
||||
# (например, прямо на RPi-хосте)
|
||||
```
|
||||
|
||||
## Что сознательно не автоматизировано
|
||||
|
||||
- `scripts/test_file_link.sh` — e2e-передача файла через радио с md5-сверкой:
|
||||
нужны две живые Pluto-платы по SSH/RF.
|
||||
- `scripts/deploy.sh`, `make deploy` — заливка бинарников на платы.
|
||||
- On-board `ofdm_bench`, PER-свипы, соук-тесты — числа с x86 бессмысленны:
|
||||
бюджет CPU в CLAUDE.md посчитан для Cortex-A9 @ 667 МГц, а не для сервера.
|
||||
Правило #6 в CLAUDE.md: при изменении PHY-параметров замерять на плате и
|
||||
обновлять `docs/BENCHMARK.md` в том же коммите — CI это не проверяет.
|
||||
|
||||
Зелёные 01–04 подтверждают компилируемость, статические инварианты и
|
||||
group-FEC математику. Они **не** подтверждают, что радиолиния действительно
|
||||
передаёт данные по эфиру — это по-прежнему ручной чек-лист выше.
|
||||
|
||||
## Файлы
|
||||
|
||||
- `.gitea/workflows/{01-smoke,02-guardrails,03-gftest,04-cross-build}.yml`
|
||||
- `scripts/ci_guardrails.sh` — логика 02, гоняется и локально
|
||||
- `runner-image/Dockerfile`, `docker-compose.runner-armhf.yml`,
|
||||
`entrypoint.sh` cross-builder'а — **вне этого репозитория**, на сервере в
|
||||
`/opt/gitea-runner-armhf/`. Не версионируются здесь; если понадобится
|
||||
восстановить cross-builder с нуля, конфигурация существует только на
|
||||
сервере.
|
||||
Reference in New Issue
Block a user