diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 8bc2c58..9c87e9e 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -58,7 +58,7 @@ jobs: fi VERSION="commit-$(echo '${{ gitea.sha }}' | cut -c1-7)" GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \ - go build -ldflags="-s -w -X main.version=${VERSION}" -o "${FILENAME}" ./cmd/ratatoskr/ + 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/Makefile b/Makefile index b4073ac..7fa6f99 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,8 @@ CMD_DIR := ./cmd/ratatoskr GO ?= go GOPATH := $(shell $(GO) env GOPATH) GIT_SHA := $(shell git rev-parse --short HEAD 2>/dev/null || echo dev) -GOFLAGS ?= -ldflags="-s -w -X main.version=commit-$(GIT_SHA)" +UPDATE_TOKEN ?= "" +GOFLAGS ?= -ldflags="-s -w -X main.version=commit-$(GIT_SHA) -X main.updateToken=$(UPDATE_TOKEN)" all: test build diff --git a/cmd/ratatoskr/main.go b/cmd/ratatoskr/main.go index c931399..79e289b 100644 --- a/cmd/ratatoskr/main.go +++ b/cmd/ratatoskr/main.go @@ -19,6 +19,11 @@ import ( // Локальная сборка без ldflags оставляет "dev". var version = "dev" +// updateToken — токен read:package для авто-обновления, вшивается при сборке: +// go build -ldflags "-X main.updateToken=" +// Без ldflags — пустой; тогда используется update.token из конфига (если задан). +var updateToken = "" + func main() { cfg := flag.String("config", "", "путь к config.yaml (по умолчанию — CWD/config.yaml)") versionFlag := flag.Bool("version", false, "показать версию и выйти") @@ -29,7 +34,7 @@ func main() { return } - a, err := app.New(*cfg, version) + a, err := app.New(*cfg, version, updateToken) if err != nil { log.Fatalf("app init: %v", err) } diff --git a/config.yaml.example b/config.yaml.example index 0041d18..bce2849 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -20,7 +20,10 @@ telegram: # update (авто-обновление бинаря из Gitea Packages) # Авто-режим только проверяет и уведомляет владельца; замена — по команде /update. +# Если base_url не задан — обновление отключено (фоллбэк на git.base_url НЕ используется). # enabled: false # включить фоновую проверку обновления +# base_url: "http://gitea.hal9000.home" # хост Gitea для обновления (UPDATE_BASE_URL) +# token: "${UPDATE_TOKEN}" # токен read:package (UPDATE_TOKEN) — опционально, если не вшит # owner: "kamelion" # владелец пакета в Gitea (UPDATE_OWNER) # package: "ratatoskr" # имя пакета-хранилища (generic) # check_interval: "24h" # как часто проверять наличие обновления diff --git a/internal/app/app.go b/internal/app/app.go index 2f30f41..52e5b86 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -38,7 +38,10 @@ type App struct { // New читает конфиг и собирает все зависимости. // Не запускает подсистемы (Run). -func New(configPath, version string) (*App, error) { +// version — вшитая версия бинаря (ldflag -X main.version). +// updateToken — вшитый токен read:package для авто-обновления +// (ldflag -X main.updateToken); имеет приоритет над update.token из конфига. +func New(configPath, version, updateToken string) (*App, error) { cfg, err := config.Load(configPath) if err != nil { return nil, fmt.Errorf("%w: %v", ErrConfig, err) @@ -113,12 +116,17 @@ func New(configPath, version string) (*App, error) { a.Router = router a.Worker = w - // Auto-обновление: .new/.old рядом с бинарником (Dir пуст → binDir() от os.Executable) + // Auto-обновление: .new/.old рядом с бинарником (Dir пуст → binDir() от os.Executable). + // Токен: вшитый updateToken приоритетнее update.token из конфига. + udToken := updateToken + if udToken == "" { + udToken = cfg.Update.Token + } ud := &update.Updater{ - BaseURL: cfg.Git.BaseURL, + BaseURL: cfg.Update.BaseURL, Owner: cfg.Update.Owner, Package: cfg.Update.Package, - Token: cfg.Git.Token, + Token: udToken, CurrentVersion: version, } a.Updater = ud @@ -369,7 +377,7 @@ func (a *App) version() string { // При активной задаче в чате — требует подтверждения, чтобы не прерывать работу. func (a *App) handleUpdateCmd(ctx context.Context, uid chat.UserID) { if a.Updater == nil || a.Updater.BaseURL == "" || a.Updater.Owner == "" { - a.send(ctx, uid, "Обновление не настроено (нужен git.base_url и update.owner в config).") + a.send(ctx, uid, "Обновление не настроено (нужен update.base_url и update.owner в config).") return } if !a.Config.Update.Enabled { diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 266f5f2..d33ec49 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -20,7 +20,7 @@ func TestNew(t *testing.T) { t.Setenv("RATATOSKR_DB", dbPath) // Загружаем без config-файла (дефолты + env) - a, err := New("", "dev") + a, err := New("", "dev", "") if err != nil { t.Fatalf("New() err = %v", err) } @@ -50,7 +50,7 @@ func TestNew_MissingToken(t *testing.T) { t.Setenv("TG_CHAT_ID", "12345") t.Setenv("RATATOSKR_DB", dbPath) - _, err := New("", "dev") + _, err := New("", "dev", "") if err == nil { t.Fatal("expected error for missing token") } @@ -65,7 +65,7 @@ func TestNew_BadDB(t *testing.T) { t.Setenv("TG_CHAT_ID", "12345") t.Setenv("RATATOSKR_DB", dbPath) - _, err := New("", "dev") + _, err := New("", "dev", "") if err == nil { t.Fatal("expected error for invalid db path") } @@ -82,7 +82,7 @@ func TestNew_RunCtxCancel(t *testing.T) { t.Setenv("TG_CHAT_ID", "12345") t.Setenv("RATATOSKR_DB", dbPath) - a, err := New("", "dev") + a, err := New("", "dev", "") if err != nil { t.Fatalf("New() err = %v", err) } @@ -106,4 +106,57 @@ func TestMain(t *testing.T) { t.Fatalf("main.go not found: %v", err) } t.Log("main.go exists") +} + +// TestNew_UpdateWiring проверяет: 1) update.base_url берётся из update-блока +// (не git.base_url); 2) приоритет токена: вшитый updateToken > update.token. +func TestNew_UpdateWiring(t *testing.T) { + tmp := t.TempDir() + dbPath := filepath.Join(tmp, "test.db") + + configPath := filepath.Join(tmp, "config.yaml") + content := strings.Join([]string{ + "telegram:", + " token: \"test:token\"", + " chat_id: \"12345\"", + "git:", + " base_url: \"http://local-git/\"", + " token: \"git-token\"", + "update:", + " enabled: true", + " base_url: \"https://hub.example.com\"", + " token: \"cfg-update-token\"", + " owner: \"kamelion\"", + "paths:", + " db: \"" + dbPath + "\"", + "", // пустая строка в конце + }, "\n") + if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil { + t.Fatalf("write config: %v", err) + } + + // 1) base_url из update-блока, а НЕ git + a, err := New(configPath, "dev", "") + if err != nil { + t.Fatalf("New() err = %v", err) + } + if a.Updater == nil { + t.Fatal("Updater не создан") + } + if want := "https://hub.example.com"; a.Updater.BaseURL != want { + t.Errorf("BaseURL = %q, want %q (должен быть update.base_url, не git.base_url)", a.Updater.BaseURL, want) + } + // без вшитого токена берём update.token из конфига + if a.Updater.Token != "cfg-update-token" { + t.Errorf("Token = %q, want cfg-update-token (при пустом вшитом)", a.Updater.Token) + } + + // 2) вшитый updateToken перекрывает оба источника + a2, err := New(configPath, "dev", "embedded-update-token") + if err != nil { + t.Fatalf("New() err = %v", err) + } + if a2.Updater.Token != "embedded-update-token" { + t.Errorf("Token = %q, want embedded-update-token (вшитый приоритетнее)", a2.Updater.Token) + } } \ No newline at end of file diff --git a/internal/config/types.go b/internal/config/types.go index 5c68138..f640eaa 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -50,12 +50,17 @@ type GitCfg struct { // UpdateCfg — автообновление. // Enabled=false отключает и авто-проверку, и /update. +// BaseURL — хост Gitea для обновления (обязателен: без него update отключён, +// фоллбэк на git.base_url НЕ используется). +// Token — токен read:package. Приоритет: вшитый -X main.updateToken > этот. // Owner — владелец Gitea-пакета (обязателен для работы обновления). // Package — имя пакета-хранилища (по умолчанию ratatoskr). // CheckInterval — как часто авто-проверять наличие обновления // (по умолчанию 24h). Авто-режим ТОЛЬКО уведомляет; замена — по /update. type UpdateCfg struct { Enabled bool `yaml:"enabled" default:"false"` + BaseURL string `yaml:"base_url" env:"UPDATE_BASE_URL"` + Token string `yaml:"token" env:"UPDATE_TOKEN"` Owner string `yaml:"owner" env:"UPDATE_OWNER"` Package string `yaml:"package" default:"ratatoskr"` CheckInterval Duration `yaml:"check_interval" default:"24h"`