diff --git a/.gitea/workflows/firmware.yml b/.gitea/workflows/firmware.yml new file mode 100644 index 00000000..424cddb1 --- /dev/null +++ b/.gitea/workflows/firmware.yml @@ -0,0 +1,146 @@ +name: Firmware build + +# Сборка прошивок на своём Gitea-раннере (метка ubuntu-latest, см. ci/README.md). +# +# push в main — собрать набор прошивок и выгрузить артефакты +# push тега — то же плюс черновик релиза с прикреплёнными файлами +# вручную — можно задать targets / match / version + +on: + push: + branches: + - main + tags: + - 'v*' + - '*-v*' + paths-ignore: + - '**/*.md' + - '.preview/**' + - 'ci/**' + workflow_dispatch: + inputs: + targets: + description: 'env через запятую (пусто — набор по умолчанию)' + required: false + default: '' + match: + description: 'подстрока имени env, например heltec_t096' + required: false + default: '' + version: + description: 'FIRMWARE_VERSION (по умолчанию ci)' + required: false + default: '' + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 300 + steps: + - name: Клонирование + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Кэш PlatformIO + uses: actions/cache@v4 + continue-on-error: true # сервер кэша может быть недоступен — сборку это ломать не должно + with: + path: | + ~/.cache/pip + ~/.platformio + ~/.platformio-venv + key: pio-${{ runner.os }}-v1 + restore-keys: | + pio-${{ runner.os }}- + + - name: PlatformIO + run: | + # отдельное venv: на свежих Debian/Ubuntu pip в систему запрещён (PEP 668) + python3 -m venv "$HOME/.platformio-venv" + "$HOME/.platformio-venv/bin/pip" install --upgrade pip platformio + echo "$HOME/.platformio-venv/bin" >> "$GITHUB_PATH" + + - name: Версия прошивки + run: | + # тег вида companion-v1.2.3 даёт версию v1.2.3; иначе — значение из запуска + if [[ "${GITHUB_REF}" == refs/tags/* ]]; then + TAG="${GITHUB_REF#refs/tags/}" + VERSION="${TAG##*-}" + else + VERSION="${{ github.event.inputs.version }}" + [[ -z "$VERSION" ]] && VERSION="ci" + fi + echo "FIRMWARE_VERSION=$VERSION" >> "$GITHUB_ENV" + echo "версия: $VERSION" + + - name: Сборка прошивок + env: + TARGETS: ${{ github.event.inputs.targets }} + MATCH: ${{ github.event.inputs.match }} + run: bash ci/build-firmwares.sh + + - name: Выгрузка артефактов + uses: actions/upload-artifact@v4 + if: always() + with: + name: firmware + path: out/ + if-no-files-found: warn + + release: + needs: build + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Скачивание артефактов + uses: actions/download-artifact@v4 + with: + name: firmware + path: out + + - name: Черновик релиза + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + TAG="${GITHUB_REF#refs/tags/}" + API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}" + + if [[ -z "${TOKEN:-}" ]]; then + echo "Нет токена для Gitea API (secrets.GITHUB_TOKEN пуст)" >&2 + exit 1 + fi + + echo "файлы к релизу:" + ls -1 out + + # черновик: релиз создаётся, но не публикуется — проверить и нажать Publish + RESPONSE=$(curl -fsS -X POST "$API/releases" \ + -H "Authorization: token $TOKEN" \ + -H 'Content-Type: application/json' \ + -d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"draft\":true,\"prerelease\":false,\"target_commitish\":\"$GITHUB_SHA\"}") + + # id разбираем питоном: в ответе есть и другие поля id (автор, вложения) + RELEASE_ID=$(printf '%s' "$RESPONSE" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])') + + if [[ -z "$RELEASE_ID" ]]; then + echo "Не удалось создать релиз для тега $TAG" >&2 + exit 1 + fi + echo "релиз #$RELEASE_ID создан (черновик)" + + for f in out/*; do + [[ -f "$f" ]] || continue + name="$(basename "$f")" + echo " прикрепляю $name" + curl -fsS -X POST "$API/releases/$RELEASE_ID/assets?name=$name" \ + -H "Authorization: token $TOKEN" \ + -F "attachment=@$f" > /dev/null + done + + echo "готово: ${{ github.server_url }}/${{ github.repository }}/releases" diff --git a/ci/README.md b/ci/README.md new file mode 100644 index 00000000..c5b324c0 --- /dev/null +++ b/ci/README.md @@ -0,0 +1,165 @@ +# CI: автоматическая сборка прошивок на своём Gitea + +Схема: **Gitea (systemd) → act_runner на том же сервере → PlatformIO → артефакты `out/`**. +Сборка идёт через `build.sh` из репозитория, то есть тем же путём, что и локально +(версия прошивки, `merge-bin` для ESP32, `.uf2` для nRF52), — логика не дублируется. + +## Что где лежит + +| Файл | Назначение | +|---|---| +| `.gitea/workflows/firmware.yml` | workflow: клон, кэш PlatformIO, сборка, артефакты, черновик релиза по тегу | +| `ci/build-firmwares.sh` | сборка выбранного набора env (годится и для запуска руками) | +| `ci/act_runner/act-runner.service` | systemd-юнит раннера | +| `ci/act_runner/config.yaml` | конфиг раннера: ёмкость, таймаут, кэш | +| `ci/act_runner/docker-compose.yml` | альтернатива: раннер в Docker | + +## 1. Что должно быть на VPS + +```bash +apt-get update +apt-get install -y git python3 python3-venv python3-pip build-essential curl ca-certificates +``` + +- `python3-venv` обязателен: workflow ставит PlatformIO в отдельное окружение + (`~/.platformio-venv`), потому что на свежих Debian/Ubuntu pip в систему запрещён. +- Свободного места нужно **5 ГБ**: `~/.platformio` с тулчейнами (~2–3 ГБ), + venv и промежуточные файлы сборки. +- RAM: 2 ГБ достаточно, при 1 ГБ возможны падения линковки ESP32. + +## 2. Установка раннера (systemd) + +```bash +# отдельный пользователь, от его имени пойдут сборки +useradd --create-home --home-dir /var/lib/act-runner --shell /bin/bash act-runner + +# бинарник: актуальную версию и имя файла сверьте на +# https://gitea.com/gitea/act_runner/releases +# (в свежих релизах проект называется Gitea Runner, см. https://docs.gitea.com/runner/) +curl -fsSL -o /usr/local/bin/act_runner \ + https://gitea.com/gitea/act_runner/releases/download/v0.2.11/act_runner-0.2.11-linux-amd64 +chmod +x /usr/local/bin/act_runner + +# каталоги +install -d -o act-runner -g act-runner /var/lib/act-runner +install -d /etc/act_runner +cp ci/act_runner/config.yaml /etc/act_runner/config.yaml + +# токен: Gitea → репозиторий → Settings → Actions → Runners → Create new Runner +su - act-runner -c 'act_runner register --no-interactive \ + --instance https://git.meshinfo.ru \ + --token ВСТАВИТЬ_ТОКЕН \ + --name meshcore-builder \ + --labels ubuntu-latest:host' + +# сервис +cp ci/act_runner/act-runner.service /etc/systemd/system/ +systemctl daemon-reload +systemctl enable --now act-runner +systemctl status act-runner +journalctl -u act-runner -f # в логах: "runner registered" / "polling tasks" +``` + +Метка `ubuntu-latest:host` означает, что шаги выполняются **прямо на сервере** — быстро +и без Docker, но зависимости ставятся в систему. Метка должна совпадать с `runs-on` +в `.gitea/workflows/firmware.yml`. + +Раннер виден в Gitea: репозиторий → Settings → Actions → Runners (должен быть зелёным). + +### Альтернатива: раннер в Docker + +Если на VPS есть Docker, из `ci/act_runner`: + +```bash +cd ci/act_runner +cat > .env <<'EOF' +GITEA_INSTANCE_URL=https://git.meshinfo.ru +GITEA_RUNNER_REGISTRATION_TOKEN=ВСТАВИТЬ_ТОКЕН +GITEA_RUNNER_NAME=meshcore-builder +EOF +docker compose up -d && docker compose logs -f +``` + +В этом варианте job'ы идут в контейнере `catthehacker/ubuntu:act-22.04`, а docker.sock +смонтирован внутрь раннера. В свежих версиях образ может называться `gitea/runner`. + +## 3. Отключить лишние workflow + +Gitea читает workflow и из `.gitea/workflows`, и из `.github/workflows`, а там лежат +upstream-файлы с триггером `push`: `build-companion-firmwares.yml`, +`build-repeater-firmwares.yml`, `build-room-server-firmwares.yml`, `github-pages.yml`, +`run-unit-tests.yml`. На первом же пуше они попытаются собрать сотни env (в проекте их +около 600) или задеплоить Pages. + +Отключите их в настройках репозитория (раздел Actions) либо через API: + +```bash +curl -X POST -H "Authorization: token $GITEA_TOKEN" \ + https://git.meshinfo.ru/api/v1/repos/shade/MeshCore/actions/workflows/run-unit-tests.yml/disable +``` + +Оставьте включённым только `firmware.yml`. Если ваша версия Gitea поддерживает +настройку `actions.WORKFLOW_DIRS`, можно ограничить список каталогов одним +`.gitea/workflows` — тогда upstream-файлы не подхватятся вовсе. + +## 4. Запуск сборки + +- **Автоматически** — push в `main` (кроме изменений в `*.md`, `ci/**`, `.preview/**`). +- **Вручную** — Actions → Firmware build → Run workflow, поля: + - `targets` — env через запятую, например `Heltec_t096_companion_radio_ble,ProMicro_companion_radio_ble`; + - `match` — подстрока имени env, например `heltec_t096`; + - `version` — значение `FIRMWARE_VERSION` (по умолчанию `ci`). +- **Локально** — тот же скрипт, что запускает CI: + +```bash +FIRMWARE_VERSION=v1.0.0 bash ci/build-firmwares.sh +MATCH=promicro bash ci/build-firmwares.sh +TARGETS="Heltec_t096_companion_radio_ble" bash ci/build-firmwares.sh +bash build.sh list | grep -i t096 # доступные env +``` + +Готовые файлы попадают в `out/` (каталог в `.gitignore`) и прикрепляются к запуску +workflow как артефакт `firmware`. + +## 5. Релизы по тегу + +Пуш тега вида `v1.0.0` или `companion-v1.0.0` запускает сборку и **создаёт черновик +релиза** с прикреплёнными `.bin` / `.uf2` / `.zip`. Версия прошивки берётся из тега: +`companion-v1.2.3` → `v1.2.3`, и попадает в имена файлов +(`-v1.2.3-.uf2`). + +Черновик — чтобы можно было посмотреть состав и нажать Publish руками: + +```bash +git tag companion-v1.0.0 +git push origin companion-v1.0.0 +# затем: репозиторий → Releases → черновик → Publish +``` + +Для публикации используется `secrets.GITHUB_TOKEN`, который Gitea выдаёт workflow +автоматически (в workflow стоит `permissions: contents: write`). Если по вашему +токену релизы не создаются, заведите секрет репозитория `RELEASE_TOKEN` с правом +`write:repository` и подставьте его в шаге «Черновик релиза». + +## Набор прошивок по умолчанию + +`ci/build-firmwares.sh` собирает устройства этого форка (список — массив +`DEFAULT_TARGETS`): T096 (companion, repeater, room server, sensor), ProMicro +(SSD1306, SH1106, repeater, room server), Heltec v3, heltec_v4 TFT, tracker_v2, +T-Deck, T-Echo Card, T114, T190, M9 и три варианта Meshadventurer. + +## Время и кэш + +- Первая (холодная) сборка набора — примерно 30–60 минут: скачиваются тулчейны. +- Последующие запуски заметно быстрее: `~/.platformio` и venv кэшируются между + запусками через `actions/cache`. +- `capacity: 1` в `config.yaml` — одна сборка за раз; поднимайте, если CPU позволяет. + +## Ограничения + +- Шаги используют `actions/checkout`, `actions/cache`, `actions/upload-artifact`, + `actions/download-artifact`. Раннер скачивает их с GitHub (`DEFAULT_ACTIONS_URL`), + поэтому серверу нужен доступ в интернет; в закрытом контуре укажите свой миррор + экшенов в `config.yaml`. +- Кэш `~/.platformio` растёт со временем: если диск кончается, почистите + `~/.platformio/.cache` у пользователя `act-runner`. diff --git a/ci/act_runner/act-runner.service b/ci/act_runner/act-runner.service new file mode 100644 index 00000000..2b951e1a --- /dev/null +++ b/ci/act_runner/act-runner.service @@ -0,0 +1,22 @@ +[Unit] +Description=Gitea Actions runner (MeshCore firmware builder) +Documentation=https://docs.gitea.com/usage/actions/act-runner +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +# отдельный пользователь: от его имени выполняются шаги workflow, +# здесь же живёт кэш PlatformIO +User=act-runner +Group=act-runner +WorkingDirectory=/var/lib/act-runner +Environment=HOME=/var/lib/act-runner +ExecStart=/usr/local/bin/act_runner daemon --config /etc/act_runner/config.yaml +Restart=always +RestartSec=5 +# сборка прошивок — процесс долгий и прожорливый, но не привилегированный +NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target diff --git a/ci/act_runner/config.yaml b/ci/act_runner/config.yaml new file mode 100644 index 00000000..6dec6ae6 --- /dev/null +++ b/ci/act_runner/config.yaml @@ -0,0 +1,30 @@ +# Конфигурация act_runner (Gitea Actions). +# Подключается в docker-compose.yml как /config.yaml. + +log: + level: info + +runner: + file: /data/.runner + capacity: 1 # одновременно одна сборка; поднимайте, если на VPS много CPU + timeout: 5h # холодная сборка набора прошивок может идти долго + fetch_timeout: 30s + fetch_interval: 5s + # Метки не задаём здесь: они фиксируются при регистрации раннера + # (systemd: act_runner register --labels ubuntu-latest:host, + # docker: переменная GITEA_RUNNER_LABELS в docker-compose.yml) + +cache: + enabled: true + dir: /data/cache + host: "" + port: 0 + +container: + # job'ы запускаются как sibling-контейнеры через смонтированный docker.sock + network: "" + privileged: false + options: "--add-host=host.docker.internal:host-gateway" + workdir_parent: "" + valid_volumes: [] + force_pull: false diff --git a/ci/act_runner/docker-compose.yml b/ci/act_runner/docker-compose.yml new file mode 100644 index 00000000..d8817f4e --- /dev/null +++ b/ci/act_runner/docker-compose.yml @@ -0,0 +1,32 @@ +# act_runner для Gitea: выполняет Gitea Actions на этом сервере. +# +# Запуск (из каталога ci/act_runner): +# 1) получить токен: Gitea → репозиторий → Settings → Actions → Runners → Create new Runner +# 2) создать .env рядом с этим файлом: +# GITEA_INSTANCE_URL=https://git.meshinfo.ru +# GITEA_RUNNER_REGISTRATION_TOKEN=<токен из шага 1> +# GITEA_RUNNER_NAME=meshcore-builder +# 3) docker compose up -d +# 4) проверить: docker compose logs -f +# +# Метка ubuntu-latest должна совпадать с runs-on в .gitea/workflows/firmware.yml. +# Образ job'а — catthehacker/ubuntu:act-22.04: в нём уже есть git, curl, python3, +# build-essential; PlatformIO ставится шагом workflow и живёт в кэше. + +services: + act-runner: + image: gitea/act_runner:latest + container_name: meshcore-act-runner + restart: unless-stopped + environment: + GITEA_INSTANCE_URL: ${GITEA_INSTANCE_URL:?укажите адрес Gitea в .env} + GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN:?нужен токен раннера} + GITEA_RUNNER_NAME: ${GITEA_RUNNER_NAME:-meshcore-builder} + GITEA_RUNNER_LABELS: ${GITEA_RUNNER_LABELS:-ubuntu-latest:docker://catthehacker/ubuntu:act-22.04} + CONFIG_FILE: /config.yaml + volumes: + # состояние раннера (.runner) и локальный кэш Gitea Actions + - ./data:/data + - ./config.yaml:/config.yaml:ro + # job'ы запускаются как соседние контейнеры, поэтому нужен доступ к демону + - /var/run/docker.sock:/var/run/docker.sock diff --git a/ci/build-firmwares.sh b/ci/build-firmwares.sh new file mode 100644 index 00000000..3bbd7e35 --- /dev/null +++ b/ci/build-firmwares.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# +# Сборка выбранных прошивок через build.sh — тот же путь, что и при локальной сборке, +# поэтому CI не дублирует логику (версия, merge-bin, uf2, каталог out/). +# Скрипт годится и для запуска руками на VPS или локально. +# +# Переменные окружения: +# TARGETS список env через запятую (перебивает набор по умолчанию) +# MATCH подстрока имени env — собрать все подходящие (регистронезависимо) +# FIRMWARE_VERSION версия прошивки (по умолчанию ci) +# OUT_DIR каталог артефактов (по умолчанию out, его использует build.sh) +# +# Примеры: +# bash ci/build-firmwares.sh +# TARGETS="Heltec_t096_companion_radio_ble,ProMicro_companion_radio_ble" bash ci/build-firmwares.sh +# MATCH=heltec_t096 FIRMWARE_VERSION=v1.2.3 bash ci/build-firmwares.sh +# +# Один упавший env не отменяет остальные: в конце печатается список несобравшихся, +# и скрипт возвращает ненулевой код, чтобы CI пометил сборку красной. + +set -uo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/.." + +# Набор по умолчанию — устройства этого форка. +DEFAULT_TARGETS=( + # companion + Heltec_t096_companion_radio_ble + ProMicro_companion_radio_ble + ProMicro_SH1106_companion_radio_ble + Heltec_v3_companion_radio_ble + heltec_v4_tft_companion_radio_ble + heltec_tracker_v2_companion_radio_ble + LilyGo_TDeck_companion_radio_ble + LilyGo_T-Echo_Card_companion_radio_ble + Heltec_t114_companion_radio_ble + Heltec_T190_companion_radio_ble_ + ThinkNode_M9_companion_radio_ble_ + Meshadventurer_sx1262_companion_radio_ble + Meshadventurer_sx1262_e22p_868m30s_companion_radio_ble + Meshadventurer_e22p_433m30s_companion_radio_ble + # repeater / room server / sensor + Heltec_t096_repeater + Heltec_t096_room_server + Heltec_t096_sensor + ProMicro_repeater + ProMicro_room_server +) + +export FIRMWARE_VERSION="${FIRMWARE_VERSION:-ci}" +OUT_DIR="${OUT_DIR:-out}" +mkdir -p "$OUT_DIR" + +targets=() +if [[ -n "${MATCH:-}" ]]; then + # список env берём тем же способом, что и build.sh list + while IFS= read -r env; do + [[ -n "$env" ]] && targets+=("$env") + done < <(bash build.sh list | grep -i -- "$MATCH" || true) +elif [[ -n "${TARGETS:-}" ]]; then + IFS=',' read -r -a parts <<< "$TARGETS" + for part in "${parts[@]}"; do + part="$(printf '%s' "$part" | xargs)" + [[ -n "$part" ]] && targets+=("$part") + done +else + targets=("${DEFAULT_TARGETS[@]}") +fi + +if [[ ${#targets[@]} -eq 0 ]]; then + echo "Нечего собирать: проверьте TARGETS/MATCH" >&2 + exit 1 +fi + +echo "Версия прошивки: $FIRMWARE_VERSION" +echo "К сборке: ${#targets[@]}" +printf ' %s\n' "${targets[@]}" + +failed=() +for env in "${targets[@]}"; do + echo + echo "===== $env =====" + if bash build.sh build-firmware "$env"; then + echo "----- $env: OK" + else + echo "----- $env: ОШИБКА" + failed+=("$env") + fi +done + +echo +echo "===== итог =====" +echo "собрано ${#targets[@]} - ${#failed[@]} ошибок" +if [[ ${#failed[@]} -gt 0 ]]; then + printf 'не собрались: %s\n' "${failed[@]}" +fi +echo "артефакты в $OUT_DIR:" +ls -1 "$OUT_DIR" 2>/dev/null || echo " (пусто)" + +[[ ${#failed[@]} -eq 0 ]]