Участие в разработке¶
Проект открытый, под лицензией AGPL-3.0. Основной репозиторий — GitLab, merge request’ы отправляйте туда.
Обязательное: подпись коммитов¶
CI проверяет каждый коммит на наличие трейлера Signed-off-by: и падает,
если его нет:
git commit -s -m "Краткое описание изменения"
Забыли — поправьте историю до отправки: git rebase --signoff <база>.
Соглашения кода¶
Линтер —
ruff:ruff check .
Настройки в
pyproject.toml: строка до 88 символов, двойные кавычки, явный список правил вselect. Версия ruff запинена и продублирована в.gitlab-ci.yml(GITLAB_CI_RUFF_VERSION) — обновлять в двух местах сразу. Пин и явныйselectне формальность: ruff расширяет набор правил по умолчанию от релиза к релизу, и незакреплённая версия ломала CI без единой правки кода.Язык. Сообщения логов, комментарии и докстринги — русские. Логирование только с ленивым форматированием:
logger.info("Наблюдение начато: %s", observation_id) # так logger.info(f"Наблюдение начато: {observation_id}") # не так
Импорты.
src/— это два корня,coreиsoniks_client. Импорты абсолютные от них; префиксsrc.не пишем никогда — в контейнере эти пакеты лежат прямо в/src.Ошибки логируем и гасим. Сломанный проход не должен ронять планировщик. Исключения из
core/exceptions.pyнужны для выбора судьбы файлов, а не для всплытия наверх — см. Загрузку данных.Докстринги — стиль Google (napoleon), на русском. Модульный докстринг обязателен: он попадает в Справочник API.
Shell-скрипты проверяются
shellcheck --severity=warning, Dockerfile —hadolint. Обе проверки есть в CI отдельными джобами.
Тесты¶
uv run --python 3.11 --extra dev python -m pytest tests -q
pytest и ruff объявлены в [project.optional-dependencies].dev и в
виртуальном окружении проекта не стоят — отсюда --extra dev.
Важно
Джоба pytest в CI ставит зависимости командой uv sync --locked, то есть
строго по uv.lock, а не перерешивает диапазоны из pyproject.toml. Отсюда
правило: правка зависимости требует uv lock в том же коммите, иначе
джоба падает с «lockfile needs to be updated». Это и есть цель — до пина CI
краснел от чужих релизов без единой правки кода (так было со skyfield 1.55,
см. roadmap.md). Версия самого uv запинена в .gitlab-ci.yml
(GITLAB_CI_UV_VERSION): uv.lock имеет revision = 3, и старый uv его
не прочитает.
Джоба pages осознанно осталась на pip: её зависимости живут в
docs/requirements.txt, которого в uv.lock нет.
Изоляция от вашего .env — в tests/conftest.py: он подменяет файл настроек
через переменную SONIKS_ENV_FILE, выставляет обязательные STATION__* и
записываемые пути, а также подменяет Hamlib на MagicMock — пакет собирается
только внутри образа, а soniks_client.jobs тянет его по цепочке импортов.
Задавать что-либо в командной строке больше не нужно.
Один тест:
uv run --python 3.11 --extra dev python -m pytest \
tests/antenna/tracking/test_strategies.py::TestFlipStrategy -q
Покрыты стратегии слежения и угловая математика, выбор пролёта из событий
Skyfield, судьба файлов по итогу выгрузки — включая все три пути выгрузки
(во время прохода, после него и повтор из incomplete), накопление кадров
в пачку, разбор KISS и скрипты-декодеры, синхронизация расписания, разбор
.dat водопада и границы его шкалы, выбор приёмника по частоте, имена файлов
наблюдения, постобработка прохода (судьба сырого .dat и блок signal
в метаданных), командная строка скриптов вокруг прохода и потокового графа
вместе с их запуском и остановкой, сверка env-имён в scripts/ с полями
Settings, таблица соответствия режимов демодуляции, а также отсутствие
побочных эффектов
у импорта (tests/test_import_side_effects.py проверяет это отдельным
процессом: импорт не создаёт директорий, configure_runtime() создаёт).
Всё, что требует живого SDR или Hamlib, по-прежнему проверяется только
в контейнере.
Проверка изменений¶
Без SDR полноценно проверить клиент нельзя. Реальные способы:
ruff check .— то же, что сделает CI.Тесты стратегий — если правили угловую математику.
Локальная сборка образа —
./build.sh(версии и теги правятся внутри).docker compose upна станции — единственная настоящая проверка.scripts/test-flowgraph.shвнутри контейнера — прогон потокового графа без ожидания прохода.
Предупреждение
Корневой .env — это конфигурация реальной станции мейнтейнера с рабочим
токеном. Не коммитьте его изменения. Шаблон для правок —
client/.env.
Документация¶
Документация лежит в docs/, собирается Sphinx из Markdown (MyST).
python -m pip install . -r docs/requirements.txt
cd docs && sphinx-build -b html . _build/html
Сам проект ставится вместе с зависимостями сборки не для красоты: без
pydantic, numpy, matplotlib, apscheduler и skyfield autodoc не может
импортировать ни один модуль, и раздел api/ собирается пустым — при этом
сборка завершается успешно, только с предупреждениями.
Важно
Собирать только из docs/. Из корня репозитория pydantic подхватит
локальный .env, и сборка упадёт на его пустых значениях. conf.py
подставляет безопасное окружение сам, но .env в текущей директории имеет
приоритет.
Что стоит знать при правке:
Исходники — Markdown. Это сознательный выбор: те же файлы служат контекстом для ИИ-ассистентов, работающих с репозиторием.
Раздел Справочник API генерируется
autodocиз докстрингов. Отдельно его редактировать не нужно — пишите докстринги.Hamlibиpymcp2221при сборке документации мокаются (autodoc_mock_importsвconf.py) — их нет вне контейнера.Добавили страницу — впишите её в
toctreeвdocs/index.md, иначе Sphinx предупредит о документе вне оглавления.
CI¶
.gitlab-ci.yml, стадии static, test, docs, docker, release, security:
Задание |
Когда |
Что |
|---|---|---|
|
всегда |
Проверка трейлера |
|
всегда |
|
|
всегда |
|
|
всегда |
|
|
всегда |
|
|
основная ветка, стадия |
Сборка Sphinx, публикация документации |
|
только тег |
Сборка образа под arm64/amd64 с публикацией. Каналом поставки не является — см. предупреждение ниже |
|
расписание — сам, ветки и MR — вручную |
Сборка образа на одной платформе без публикации: проверка, что |
|
тег |
Формирование релиза |
|
тег |
Сканирование опубликованного образа |
SAST, Secret Detection, Dependency Scanning |
тег или основная ветка |
Проверки безопасности |
Предупреждение
Джоба docker не является каналом поставки и ни разу им не была. Образы
парка собираются вручную — см. «Выкатка образа» ниже. Сейчас джоба к тому же не
доходит до сборки: на раннере проекта runc не может прикрыть /proc/acpi
монтированием tmpfs и падает на старте контейнера сборки. Тег 2.3.0 есть,
образа под ним в registry нет. Это незаконченная работа, а не сломавшийся
механизм.
Мультиарх под QEMU идёт часами, поэтому джоба и привязана к тегу.
linux/arm/v7 снят — 32-битных станций нет.
Просто проверить, что образ собирается, можно и без тега: docker_build_check
собирает одну платформу без публикации. По расписанию она идёт сама, на ветках
и MR — кнопкой и не блокируя. Джоба нужна потому, что имя .deb флоуграфов и
разрешимость пиннутых тегов не видит ни один тест и ни один линтер: именно так
переименование .deb два дня роняло сборку при зелёных пайплайнах.
Версия клиента¶
Версию задаёт тег GitLab, и больше нигде она не объявляется. CI подставляет
$CI_COMMIT_TAG в ARG CLIENT_VERSION, тот перезаписывает
src/core/_version.py в образе, и порталу уходит именно это значение. В
репозитории pyproject.toml, ARG CLIENT_VERSION и _version.py содержат
заглушку 0.0.0 — руками перед релизом править нечего, достаточно поставить
тег. tests/test_version_consistency.py сторожит, чтобы настоящее число туда
не вернулось.
Рядом в _version.py образ получает коммиты gr-satnogs и флоуграфов: оба
клонируются по тегу, а хеш пишется в файл при сборке и уезжает порталу в
client_metadata.radio. Добавили в _version.py новое имя — добавьте его и в
шаг Dockerfile, иначе в образе его не окажется; тот же тест это проверяет.
Все джобы выполняются на раннере проекта: он адресуется тегом из переменной
GITLAB_CI_RUNNER_TAG в блоке default. Шаренные раннеры исчерпали квоту
compute-минут namespace и пайплайн на них не стартует. Там же задан
interruptible: true — вытесненный новым пушем пайплайн MR прерывается;
джобы по тегу помечены interruptible: false поимённо.
Выкатка образа¶
Образы парка собираются вручную и публикуются docker buildx build --push.
Инструмент — build.sh, и он же единственный канал поставки.
Образ расслоён на два: soniks-base — GNU Radio, gr-soniks, флоуграфы,
gr-satellites, apt- и pip-зависимости, гигабайты, меняется редко; поверх него
клиентский слой — код, satyaml и скрипты, десятки мегабайт, меняется часто.
Стадии base и client живут в одном Dockerfile; какую базу брать, задаёт
ARG BASE_IMAGE.
./build.sh # разработка: клиент поверх базы из BASE_IMAGE
./build.sh --base # пересобрать и базу (первый раз — обязательно)
./build.sh 2.3.0 # версия уезжает в образ и в client_metadata
./build.sh 2.3.0 --push # и публикуется тегами :2.3.0 и :latest-addons
./build.sh 2.3.0 --push --no-latest # только версионным тегом
PLATFORMS=linux/arm64,linux/amd64 ./build.sh 2.3.0 --push # обе архитектуры
Платформы. Парк — в основном RPi (linux/arm64), но есть станции на
ноутбуках с Ubuntu и Windows (linux/amd64, на Windows — Docker Desktop).
Выкатка идёт в два этапа: сначала arm64 (умолчание PLATFORMS), затем обе
архитектуры одним manifest list. Три условия, без которых мультиплатформенная
сборка не пойдёт:
чужая для машины сборки архитектура собирается под QEMU — один раз
docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64. База под QEMU собирается часами, но пересобирается редко; клиентский слой лёгкий;нужен builder с драйвером
docker-container(docker buildx create --use): драйверdockerпо умолчанию собирает только одну платформу;несколько платформ — только с
--push: manifest list в локальный docker не загружается, иbuild.shоткажет. По той же причине при--base --pushбаза публикуется первой — клиентская сборка берёт её из registry.
Что стоит знать до первой выкатки:
версия — позиционный аргумент, и без неё публиковать нельзя. Скрипт откажет: образ без версии рапортует порталу
0.0.0, наблюдения перестают делиться по версиям клиента (tools/portal_stats.py), а откатывать парк становится не на что. Для разработки версия не нужна — сборка идёт с предупреждением;публикуются два тега сразу.
latest-addonsтянут обаdocker-compose.yml— им и обновляется парк; версионный тег существует ровно затем, чтобы было куда откатиться.--no-latestпубликует только версионный: образ ложится в registry, парк его не замечает, а станцию на него переключают руками (STAND_IMAGE,image:в compose). Это форма выкатки для приёмки — сначала стенд, потом парк;база пиннится неизменяемым тегом в переменной
BASE_IMAGEвнутриbuild.sh. Плавающий тег здесь означал бы, что две сборки дают разный образ, а какой именно — не записано нигде; ровно поэтому же пиннутыGRSATNOGS_BRANCHиFLOWGRAPHS_BRANCH;новая зависимость в
pyproject.toml— это изменение базы. Ставится она на базовой стадии, поэтому такой коммит требует--base, иначе в образе её не окажется;--pushпубликует базу только если её пересобирали в этом же запуске.после
--pushскрипт печатает digest в видеsonik.space/sonikspace/soniks-client@sha256:…. Это адрес для канала релизов на портале (ReleaseChannel, админка): staff ставит его наcanary, и станции с агентом в режимеmanagedприменяют образ сами, с health-gate и откатом; тот же digest наstable— когда канарейки отработали (решение 38 в Дорожная карта: надёжность сети). Канал — это digest, не тег:latest-addonsобновляет парк без точки отката, агент тегов не принимает. Устройство агента — Агент обновлений.
Сборка идёт нативно на машине нужной архитектуры: в registry лежат
однопланформенные образы arm64 — парк на Raspberry Pi 4. Собранный на x86
образ станции не подойдёт.
Публикация документации¶
Задание pages собирает HTML в артефакт public/, который отдаётся GitLab
Pages. На sonik.space/docs/client он попадает проксированием на стороне
портала:
location /docs/client/ {
proxy_pass https://soniks-client-new-ea69a1.gitlab.io/;
proxy_set_header Host soniks-client-new-ea69a1.gitlab.io;
}
Ссылки внутри Sphinx относительные, поэтому раздача из подпути работает без дополнительной настройки.