188 lines
14 KiB
Markdown
188 lines
14 KiB
Markdown
# 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 с нуля, конфигурация существует только на
|
||
сервере.
|