From 097491bef40d15247a1258c88ad092e4ba344eee Mon Sep 17 00:00:00 2001 From: "ki.sagidullin" Date: Thu, 20 Aug 2026 17:51:25 +0500 Subject: [PATCH] =?UTF-8?q?fix(ci):=20headless=20=D0=B2=20CI,=20UI-=D0=B1?= =?UTF-8?q?=D0=B8=D0=BD=D0=B0=D1=80=D1=8C=20=D1=81=D0=BE=D0=B1=D0=B8=D1=80?= =?UTF-8?q?=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI (.gitea/workflows/ci.yaml): - go 1.23 -> 1.25 (modernc.org/sqlite v1.56.0 требует go 1.25; старый CI падал на сборке ещё до тестов). - CGO_ENABLED=0 на уровне workflow: CI собирает/тестирует только headless (ui_noui), не тянет Fyne/glfw и C-компилятор. - timeout 120s -> 300s (worker один ~30-60s на быстрой машине). - build-and-package: только linux/amd64 (headless). Windows-бинарь с UI — локально. Локальная сборка UI + публикация: - scripts/build-publish-ui.ps1: собирает ratatoskr-windows-amd64.exe (CGO_ENABLED=1, WinLibs gcc), вшивает commit- + updateToken, пишет .version/.sha256 и публикует в Gitea Packages в ту же версию, что и CI; самопроверка (U4). - scripts/.gitea-creds.example + .gitignore (scripts/.gitea-creds) — секреты не попадают в репозиторий. README: CI=headless + публикация Windows-UI локальным скриптом; обновлён intro (бинарь по умолчанию headless, окно — опционально с cgo). --- .gitea/workflows/ci.yaml | 25 ++++---- .gitignore | 3 + README.md | 57 +++++++++++++++--- scripts/.gitea-creds.example | 6 ++ scripts/build-publish-ui.ps1 | 108 +++++++++++++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 21 deletions(-) create mode 100644 scripts/.gitea-creds.example create mode 100644 scripts/build-publish-ui.ps1 diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index a796b19..eda2357 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -14,6 +14,12 @@ concurrency: group: ${{ gitea.workflow }}-${{ gitea.ref }} cancel-in-progress: true +# CI собирает/тестирует ТОЛЬКО headless-вариант: приложение без графического окна +# (internal/app/ui_noui.go). UI (Fyne-окно) требует cgo и C-компилятора, поэтому +# собирается локально и публикуется скриптом scripts/build-publish-ui.ps1. +env: + CGO_ENABLED: '0' + jobs: test: runs-on: ubuntu-latest @@ -22,11 +28,11 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.25' check-latest: true - name: Run tests - run: go test ./... -v -count=1 -timeout 120s 2>&1 + run: go test ./... -v -count=1 -timeout 300s 2>&1 - name: Vet run: go vet ./... @@ -34,27 +40,22 @@ jobs: build-and-package: if: always() needs: test - strategy: - matrix: - goos: [linux, windows] - goarch: [amd64] + # Только linux: headless-бинарь для сервера. Windows-бинарь (с UI, cgo) + # публикуется локально через scripts/build-publish-ui.ps1. runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.25' check-latest: true - name: Build binary run: | - FILENAME="ratatoskr-${{ matrix.goos }}-${{ matrix.goarch }}" - if [ "${{ matrix.goos }}" = "windows" ]; then - FILENAME="${FILENAME}.exe" - fi + FILENAME="ratatoskr-linux-amd64" VERSION="commit-$(echo '${{ gitea.sha }}' | cut -c1-7)" - GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \ + GOOS=linux GOARCH=amd64 \ go build -ldflags="-s -w -X main.version=${VERSION} -X main.updateToken=${{ secrets.TC_UPDATE_TOKEN }}" -o "${FILENAME}" ./cmd/ratatoskr/ echo "FILENAME=${FILENAME}" >> "${GITEA_ENV}" echo "VERSION=${VERSION}" >> "${GITEA_ENV}" diff --git a/.gitignore b/.gitignore index bacb527..863004a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ ratatoskr !/.env.example secrets/ +# Local Gitea credentials for scripts/build-publish-ui.ps1 +scripts/.gitea-creds + # Local config (not tracked — use config.yaml.example) config.yaml diff --git a/README.md b/README.md index 34b8f44..7f43093 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,9 @@ Ratatoskr на Go — **оркестратор в единый статический бинарь** + субагенты через [opencode](https://opencode.ai) как внешний процесс. Переезд с Python-версии: все подсистемы (диалог, воркер, автообновление) собраны в один исполняемый -файл без runtime-зависимостей (`CGO_ENABLED=0`), что упрощает доставку и деплой. +файл, что упрощает доставку и деплой. Бинарь по умолчанию headless +(`CGO_ENABLED=0`); графическое окно (Fyne) — опционально, собирается с cgo +(только локально, см. «Локальная сборка UI-бинаря»). ## Структура @@ -58,10 +60,40 @@ GOFLAGS ?= -ldflags="-s -w -X main.version=commit-$(GIT_SHA) -X main.updateToken - `main.updateToken` = токен read:package для автообновления (см. ниже). Если не задан (`UPDATE_TOKEN ?= ""`) — пустая встройка, тогда обновление берёт `update.token` из конфига. +**Обычный `make build`/`make cross` окно НЕ включает** — Fyne-окно требует cgo и +C-компилятора. Headless-бинарь (без окна) собирается и в CI, и локально; UI-бинарь +с окном — только локально (см. ниже). + +### Локальная сборка UI-бинаря (Windows, Fyne-окно) + публикация в Gitea + +CI headless: `CGO_ENABLED=0` и публикует только `ratatoskr-linux-amd64` +(см. `.gitea/workflows/ci.yaml`). Windows-бинарь с окном собирается на Windows-машине +и публикуется скриптом: + +```bash +# 1) Секреты (git-ignored): скопировать и заполнить +cp scripts/.gitea-creds.example scripts/.gitea-creds +# GITEA_TOKEN=write:packages-токен +# UPDATE_TOKEN=read:package-токен +# GIT_MAIN_URL=http://gitea.hal9000.home + +# 2) Убедиться, что gcc (MinGW) в PATH и CGO_ENABLED=1 — см. память +# `mem:toolchain/cgo-winlibs-gcc` + +# 3) Запустить (HEAD должен быть тем коммитом, что в main): +powershell -ExecutionPolicy Bypass -File scripts/build-publish-ui.ps1 +``` + +Скрипт собирает `ratatoskr-windows-amd64.exe` с `-X main.version=commit-` и +`-X main.updateToken=...`, пишет companion-файлы `.version`/`.sha256` и публикует все +три в `.../generic/ratatoskr/commit-/`. В конце сам перечитывает `.sha256`/`.version` +из Gitea и сверяет (контракт U4) — как проверка в разделе «Валидация опубликованной версии». + **Как на самом деле публикуется рабочий бинарь:** локальный `go build` токен НЕ вшивает -(значение секрета есть только у CI). Запушь `main` — CI (см. ниже) сам соберёт оба бинаря -со вшитым `TC_UPDATE_TOKEN` и загрузит их в Gitea Packages. Так соблюдён least-privilege: -read-токен не светится на хосте разработки. +(значение секрета есть только у CI и в git-ignored `scripts/.gitea-creds`). Запушь `main` — +CI сам соберёт headless-linux со вшитым `TC_UPDATE_TOKEN` и загрузит его в Gitea Packages; +Windows-бинарь с UI публикуется локальным скриптом (токен read:package в `scripts/.gitea-creds`). +Так соблюдён least-privilege: read-токен не светится в репозитории/на хосте CI. ### Проверки перед сборкой @@ -78,13 +110,17 @@ make run # build + ./ratatoskr -config config.yaml ## CI (.gitea/workflows/ci.yaml) -- **Job `test`**: checkout → setup-go → `go test ./...` → `go vet ./...`. -- **Job `build-and-package`** (matrix linux/amd64 + windows/amd64, без артефактов): - собирает бинарь со вшитым `-X main.updateToken=${secrets.TC_UPDATE_TOKEN}` и +- **Job `test`**: checkout → setup-go 1.25 → `go test ./...` → `go vet ./...`. + Работает в headless-режиме (`CGO_ENABLED=0`, без Fyne/glfw). +- **Job `build-and-package`** (linux/amd64, без артефактов): собирает **headless**-бинарь + `ratatoskr-linux-amd64` со вшитым `-X main.updateToken=${secrets.TC_UPDATE_TOKEN}` и **публикует в Gitea Packages** — только на `main`, сразу в версию `commit-/` (псевдо-версия `latest` НЕ используется — см. автообновление). Плюс companion-файлы `.version` и `.sha256`. + **Windows-бинарь с UI в CI НЕ собирается** (нужен cgo + C-компилятор). Он собирается + и публикуется локально скриптом `scripts/build-publish-ui.ps1` (см. «Локальная сборка UI-бинаря»). + Секреты/vars: `TC_GITEA_TOKEN` (write:packages), `TC_UPDATE_TOKEN` (read:package), `GIT_MAIN_URL`. @@ -199,9 +235,12 @@ update: ### Как опубликовать новую версию 1. Внеси правки, коммитни, запуши в `main`. -2. CI соберёт оба бинаря со вшитым `TC_UPDATE_TOKEN` и опубликует в +2. CI соберёт `ratatoskr-linux-amd64` (headless) со вшитым `TC_UPDATE_TOKEN` и опубликует в `.../generic/ratatoskr/commit-/` (бинарь + `.version` + `.sha256`). -3. На обновляемой машине: бот сам уведомит о новой версии → `/update`. +3. Если в этой версии есть изменения UI — дополнительно собрать и опубликовать + Windows-бинарь с окном: `scripts/build-publish-ui.ps1` (добавит + `ratatoskr-windows-amd64.exe` в ту же версию). +4. На обновляемой машине: бот сам уведомит о новой версии → `/update`. Проверку автообновления после публикации гоняет владелец на своей Windows-машине (не прогоняется агентом). diff --git a/scripts/.gitea-creds.example b/scripts/.gitea-creds.example new file mode 100644 index 0000000..183edf9 --- /dev/null +++ b/scripts/.gitea-creds.example @@ -0,0 +1,6 @@ +# Секреты для scripts/build-publish-ui.ps1 — НЕ коммитить (git-ignored). +# Скопируйте этот файл в scripts/.gitea-creds и заполните реальные значения. +# Формат: по одной `ключ=значение` на строку, строки с # — комментарии. +GITEA_TOKEN=write:packages-токен +UPDATE_TOKEN=read:package-токен +GIT_MAIN_URL=http://gitea.hal9000.home \ No newline at end of file diff --git a/scripts/build-publish-ui.ps1 b/scripts/build-publish-ui.ps1 new file mode 100644 index 0000000..f1f138c --- /dev/null +++ b/scripts/build-publish-ui.ps1 @@ -0,0 +1,108 @@ +<# +build-publish-ui.ps1 — локальная сборка Windows-бинаря с UI (Fyne) и публикация в Gitea Packages. + +Почему локально: Fyne-окно требует cgo + C-компилятор (MinGW/gcc), которых нет в CI. +CI собирает ТОЛЬКО headless-linux (см. .gitea/workflows/ci.yaml). Windows-бинарь +с графическим окном собирается на Windows-машине Камиля и публикуется скриптом ниже +в ту же версию commit-, что и CI, с теми же companion-файлами (.version/.sha256), +чтобы автообновление (U4 checksum, ResolveLatest) работало без изменений. + +Требования: + - Go с доступным C-компилятором (CGO_ENABLED=1). WinLibs gcc: см. память + `mem:toolchain/cgo-winlibs-gcc` — bin нужно добавить в PATH перед запуском. + - Файл scripts/.gitea-creds (git-ignored, см. ниже) с токенами. + Формат (по одной `ключ=значение` на строку): + GITEA_TOKEN=write:packages-токен + UPDATE_TOKEN=read:package-токен + GIT_MAIN_URL=http://gitea.hal9000.home + - Ветка уже запушена в `main` (VERSION = commit- от текущего HEAD). + +Пример: + powershell -ExecutionPolicy Bypass -File scripts/build-publish-ui.ps1 +#> + +$ErrorActionPreference = "Stop" + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoRoot = Split-Path -Parent $ScriptDir +$CredsFile = Join-Path $ScriptDir ".gitea-creds" +$Package = "ratatoskr" +$Owner = "kamelion" +$Filename = "ratatoskr-windows-amd64.exe" + +Set-Location $RepoRoot + +# --- 0. Проверка C-тулчейна --- +if ($env:CGO_ENABLED -eq "0") { Write-Error "CGO_ENABLED=0 — нужен C-компилятор (MinGW). Уберите его из env." } +if (-not (Get-Command gcc -ErrorAction SilentlyContinue)) { + Write-Host "gcc не найден в PATH. Пример (WinLibs):" + Write-Host ' $p = "<...>\mingw64\bin"; $env:Path = "$p;$env:Path"' + Write-Error "C-компилятор gcc не найден." +} +Write-Host "[ok] gcc: $((gcc --version | Select-Object -First 1))" + +# --- 1. Чтение секретов --- +if (-not (Test-Path $CredsFile)) { + Write-Error "Нет файла секретов: $CredsFile`nФормат: GITEA_TOKEN=... / UPDATE_TOKEN=... / GIT_MAIN_URL=..." +} +$creds = @{} +Get-Content $CredsFile | ForEach-Object { + $line = $_.Trim() + if ($line -and -not $line.StartsWith("#")) { + $kv = $line.Split("=", 2) + if ($kv.Count -eq 2) { $creds[$kv[0].Trim()] = $kv[1].Trim() } + } +} +$giteaToken = $creds["GITEA_TOKEN"] +$updateToken = $creds["UPDATE_TOKEN"] +$giteaUrl = $creds["GIT_MAIN_URL"] +if (-not $giteaToken) { Write-Error "В $CredsFile нет GITEA_TOKEN (нужен write:packages)." } +if (-not $giteaUrl) { Write-Error "В $CredsFile нет GIT_MAIN_URL." } + +# --- 2. Версия = commit- текущего HEAD (совпадает с именованием CI) --- +$sha = (git rev-parse --short HEAD).Trim() +if (-not $sha) { Write-Error "git rev-parse --short HEAD не дал хэш." } +$Version = "commit-$sha" +Write-Host "[build] VERSION=$Version FILENAME=$Filename" + +# --- 3. Сборка (cgo + UI) --- +$env:CGO_ENABLED = "1" +$out = Join-Path $RepoRoot $Filename +$ldflags = "-s -w -X main.version=${Version} -X main.updateToken=${updateToken}" +Write-Host "[build] go build -ldflags=... -o $Filename" +go build -ldflags=$ldflags -o $out ./cmd/ratatoskr/ +if ($LASTEXITCODE -ne 0) { Write-Error "go build завершился с кодом $LASTEXITCODE" } + +# --- 4. Companion-файлы (.version, .sha256) --- +$sha256Hex = (Get-FileHash -Algorithm SHA256 $out).Hash.ToLower() +$verFile = "$out.version" +$sumFile = "$out.sha256" +Set-Content -Path $verFile -Value $Version -NoNewline -Encoding ascii +Set-Content -Path $sumFile -Value $sha256Hex -NoNewline -Encoding ascii +Write-Host "[ok] bin: $out ($([math]::Round((Get-Item $out).Length/1MB,1)) MB)" +Write-Host "[ok] sha256: $sha256Hex" + +# --- 5. Публикация в Gitea Packages --- +$api = "$giteaUrl/api/packages/$Owner/generic/$Package/$Version" +$files = @($Filename, "$Filename.version", "$Filename.sha256") +foreach ($f in $files) { + Write-Host "[put] $api/$f" + curl.exe -sS -X PUT ` + -H "Authorization: token $giteaToken" ` + -H "Content-Type: application/octet-stream" ` + "$api/$f" ` + --data-binary "@$f" + if ($LASTEXITCODE -ne 0) { Write-Error "curl PUT $f завершился с кодом $LASTEXITCODE" } +} + +# --- 6. Самопроверка: читаем .sha256 обратно и сверяем (контракт U4) --- +Write-Host "[verify] перечитываю $Filename.sha256 из Gitea..." +$remoteSum = (curl.exe -sS -H "Authorization: token $giteaToken" "$api/$Filename.sha256").Trim() +if ($remoteSum -ne $sha256Hex) { + Write-Error "Проверка не сошлась: remote=$remoteSum local=$sha256Hex" +} +$remoteVer = (curl.exe -sS -H "Authorization: token $giteaToken" "$api/$Filename.version").Trim() +if ($remoteVer -ne $Version) { + Write-Error "Версия не сошлась: remote=$remoteVer local=$Version" +} +Write-Host "[ok] опубликовано и проверено: $Version/$Filename (+.version, +.sha256)"