14 KiB
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/. Скрипт
самодостаточен и гоняется точно так же локально:
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'а:
- 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'а
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}.ymlscripts/ci_guardrails.sh— логика 02, гоняется и локальноrunner-image/Dockerfile,docker-compose.runner-armhf.yml,entrypoint.shcross-builder'а — вне этого репозитория, на сервере в/opt/gitea-runner-armhf/. Не версионируются здесь; если понадобится восстановить cross-builder с нуля, конфигурация существует только на сервере.