refactor: migrate lint config to v2, remove daemon auto-upgrade, add trust badges
Some checks failed
Release / release (push) Failing after 1s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s

This commit is contained in:
Deivid Soto 2026-03-30 23:24:16 +02:00
parent a13104bdb7
commit efa4562acd
18 changed files with 188 additions and 268 deletions

View file

@ -73,17 +73,6 @@ func (c *Client) Deregister(ctx context.Context, agentID string) error {
return nil
}
// ReportUpgradeResult reports the outcome of a self-upgrade attempt.
func (c *Client) ReportUpgradeResult(ctx context.Context, result UpgradeResult) error {
var resp struct {
Success bool `json:"success"`
}
if err := c.doPost(ctx, "/api/internal/agent/upgrade-result", result, &resp); err != nil {
return fmt.Errorf("report upgrade: %w", err)
}
return nil
}
// ReportStatus reports download progress. Returns server-side flags the CLI must act on.
func (c *Client) ReportStatus(ctx context.Context, update StatusUpdate) (*StatusResponse, error) {
var resp StatusResponse
@ -93,6 +82,15 @@ func (c *Client) ReportStatus(ctx context.Context, update StatusUpdate) (*Status
return &resp, nil
}
// BatchReportStatus sends multiple status updates in a single request.
func (c *Client) BatchReportStatus(ctx context.Context, updates []StatusUpdate) (*BatchStatusResponse, error) {
var resp BatchStatusResponse
if err := c.doPost(ctx, "/api/internal/agent/status", BatchStatusRequest{Updates: updates}, &resp); err != nil {
return nil, fmt.Errorf("batch report status: %w", err)
}
return &resp, nil
}
// ---------------------------------------------------------------------------
// Usenet endpoints
// ---------------------------------------------------------------------------

View file

@ -324,62 +324,3 @@ func TestHeartbeatWithoutUpgradeSignal(t *testing.T) {
t.Errorf("expected no upgrade signal, got %+v", resp.Upgrade)
}
}
func TestReportUpgradeResult(t *testing.T) {
var received UpgradeResult
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/internal/agent/upgrade-result" {
t.Errorf("path = %s, want /api/internal/agent/upgrade-result", r.URL.Path)
}
if r.Method != http.MethodPost {
t.Errorf("method = %s, want POST", r.Method)
}
json.NewDecoder(r.Body).Decode(&received)
json.NewEncoder(w).Encode(struct{ Success bool }{Success: true})
}))
defer srv.Close()
c := NewClient(srv.URL, "test-key", "unarr-test")
err := c.ReportUpgradeResult(context.Background(), UpgradeResult{
AgentID: "agent-1",
Success: true,
Version: "2.0.0",
})
if err != nil {
t.Fatalf("ReportUpgradeResult failed: %v", err)
}
if received.AgentID != "agent-1" {
t.Errorf("agentId = %q, want agent-1", received.AgentID)
}
if !received.Success {
t.Error("expected success=true")
}
if received.Version != "2.0.0" {
t.Errorf("version = %q, want 2.0.0", received.Version)
}
}
func TestReportUpgradeResultFailure(t *testing.T) {
var received UpgradeResult
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewDecoder(r.Body).Decode(&received)
json.NewEncoder(w).Encode(struct{ Success bool }{Success: true})
}))
defer srv.Close()
c := NewClient(srv.URL, "test-key", "unarr-test")
err := c.ReportUpgradeResult(context.Background(), UpgradeResult{
AgentID: "agent-1",
Success: false,
Error: "checksum mismatch",
})
if err != nil {
t.Fatalf("ReportUpgradeResult failed: %v", err)
}
if received.Success {
t.Error("expected success=false")
}
if received.Error != "checksum mismatch" {
t.Errorf("error = %q, want 'checksum mismatch'", received.Error)
}
}

View file

@ -25,23 +25,26 @@ type Daemon struct {
transport Transport
// Callbacks
OnTasksClaimed func(tasks []Task)
OnStreamRequested func(req StreamRequest)
OnUpgradeRequested func(version string)
OnControlAction func(action, taskID string)
OnTasksClaimed func(tasks []Task)
OnStreamRequested func(req StreamRequest)
OnControlAction func(action, taskID string)
// State
User UserInfo
Features FeatureFlags
Info AgentInfo
State DaemonState
upgradeInProgress bool
heartbeatFailures int
User UserInfo
Features FeatureFlags
Info AgentInfo
State DaemonState
heartbeatFailures int
lastNotifiedVersion string
// Callbacks for state tracking (set by cmd/daemon.go)
GetActiveCount func() int
GetCleanableBytes func() int64
// Watching tracks whether a user is viewing download progress in the web UI.
// When false, the progress reporter skips detailed updates (only sends final states).
Watching bool
// Exposed tickers for hot-reload
PollTicker *time.Ticker
HeartbeatTicker *time.Ticker
@ -191,20 +194,18 @@ func (d *Daemon) heartbeat(ctx context.Context) {
d.heartbeatFailures = 0
}
// Update state file
// Update watching flag and state file
d.Watching = resp.Watching
d.State.LastHeartbeat = time.Now()
if d.GetActiveCount != nil {
d.State.ActiveTasks = d.GetActiveCount()
}
WriteState(&d.State)
// Check for upgrade signal from server
if resp.Upgrade != nil && resp.Upgrade.Version != "" && !d.upgradeInProgress {
d.upgradeInProgress = true
log.Printf("Upgrade requested by server: %s → %s", d.cfg.Version, resp.Upgrade.Version)
if d.OnUpgradeRequested != nil {
go d.OnUpgradeRequested(resp.Upgrade.Version)
}
// Log once per version when server suggests an upgrade
if resp.Upgrade != nil && resp.Upgrade.Version != "" && resp.Upgrade.Version != d.lastNotifiedVersion {
d.lastNotifiedVersion = resp.Upgrade.Version
log.Printf("New version available: %s (run `unarr self-update` to upgrade)", resp.Upgrade.Version)
}
}
@ -225,12 +226,9 @@ func (d *Daemon) handleEvent(event ServerEvent) {
}
case "upgrade":
if event.Upgrade != nil && event.Upgrade.Version != "" && !d.upgradeInProgress {
d.upgradeInProgress = true
log.Printf("Upgrade requested via WebSocket: %s → %s", d.cfg.Version, event.Upgrade.Version)
if d.OnUpgradeRequested != nil {
go d.OnUpgradeRequested(event.Upgrade.Version)
}
if event.Upgrade != nil && event.Upgrade.Version != "" && event.Upgrade.Version != d.lastNotifiedVersion {
d.lastNotifiedVersion = event.Upgrade.Version
log.Printf("New version available: %s (run `unarr self-update` to upgrade)", event.Upgrade.Version)
}
case "control":
@ -253,11 +251,6 @@ func (d *Daemon) TriggerPoll() {
}
}
// ClearUpgradeInProgress resets the upgrade flag so a retry can be attempted.
func (d *Daemon) ClearUpgradeInProgress() {
d.upgradeInProgress = false
}
func (d *Daemon) deregister() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

View file

@ -29,9 +29,6 @@ type Transport interface {
// Deregister notifies the server of graceful shutdown.
Deregister(ctx context.Context, agentID string) error
// ReportUpgradeResult reports upgrade outcome.
ReportUpgradeResult(ctx context.Context, result UpgradeResult) error
// Events returns a channel that emits server-initiated events.
// In HTTP mode this channel is never written to (polling handles it).
// In WS mode, tasks/upgrade/control arrive here.

View file

@ -134,23 +134,13 @@ func TestE2EFullLifecycle(t *testing.T) {
t.Fatal("timeout waiting for cancel control")
}
// 6. Send upgrade result
err = tr.ReportUpgradeResult(ctx, UpgradeResult{
AgentID: "e2e-agent",
Success: true,
Version: "2.0.0",
})
if err != nil {
t.Fatalf("ReportUpgradeResult: %v", err)
}
// Verify server received all messages
time.Sleep(100 * time.Millisecond)
mu.Lock()
defer mu.Unlock()
if len(receivedMessages) < 4 {
t.Fatalf("expected at least 4 messages, got %d", len(receivedMessages))
if len(receivedMessages) < 3 {
t.Fatalf("expected at least 3 messages, got %d", len(receivedMessages))
}
types := make([]string, len(receivedMessages))
@ -158,7 +148,7 @@ func TestE2EFullLifecycle(t *testing.T) {
types[i], _ = m["type"].(string)
}
expected := []string{"auth", "heartbeat", "progress", "upgrade-result"}
expected := []string{"auth", "heartbeat", "progress"}
for _, exp := range expected {
found := false
for _, got := range types {

View file

@ -34,6 +34,10 @@ func (t *HTTPTransport) SendProgress(ctx context.Context, update StatusUpdate) (
return t.client.ReportStatus(ctx, update)
}
func (t *HTTPTransport) BatchReportStatus(ctx context.Context, updates []StatusUpdate) (*BatchStatusResponse, error) {
return t.client.BatchReportStatus(ctx, updates)
}
func (t *HTTPTransport) ClaimTasks(ctx context.Context, agentID string) (*TasksResponse, error) {
return t.client.ClaimTasks(ctx, agentID)
}
@ -42,9 +46,5 @@ func (t *HTTPTransport) Deregister(ctx context.Context, agentID string) error {
return t.client.Deregister(ctx, agentID)
}
func (t *HTTPTransport) ReportUpgradeResult(ctx context.Context, result UpgradeResult) error {
return t.client.ReportUpgradeResult(ctx, result)
}
// Client returns the underlying HTTP client for direct use if needed.
func (t *HTTPTransport) Client() *Client { return t.client }

View file

@ -120,18 +120,6 @@ func (h *HybridTransport) Deregister(ctx context.Context, agentID string) error
return h.http.Deregister(ctx, agentID)
}
// ReportUpgradeResult delegates to the active transport.
func (h *HybridTransport) ReportUpgradeResult(ctx context.Context, result UpgradeResult) error {
if h.mode.Load() == "ws" {
if err := h.ws.ReportUpgradeResult(ctx, result); err != nil {
h.switchToHTTP()
return h.http.ReportUpgradeResult(ctx, result)
}
return nil
}
return h.http.ReportUpgradeResult(ctx, result)
}
// ── Internal ─────────────────────────────────────────────────────────────────
func (h *HybridTransport) switchToHTTP() {

View file

@ -209,22 +209,6 @@ func (t *WSTransport) Deregister(_ context.Context, _ string) error {
return t.Close()
}
// ReportUpgradeResult sends upgrade result to the DO.
func (t *WSTransport) ReportUpgradeResult(_ context.Context, result UpgradeResult) error {
msg := struct {
Type string `json:"type"`
Success bool `json:"success"`
Version string `json:"version,omitempty"`
Error string `json:"error,omitempty"`
}{
Type: "upgrade-result",
Success: result.Success,
Version: result.Version,
Error: result.Error,
}
return t.send(msg)
}
// ── Internal ─────────────────────────────────────────────────────────────────
func (t *WSTransport) send(msg any) error {

View file

@ -111,10 +111,21 @@ type StatusResponse struct {
StreamRequested bool `json:"streamRequested,omitempty"`
}
// BatchStatusRequest wraps multiple status updates in a single request.
type BatchStatusRequest struct {
Updates []StatusUpdate `json:"updates"`
}
// BatchStatusResponse wraps per-task results from the batch endpoint.
type BatchStatusResponse struct {
Results []StatusResponse `json:"results"`
}
// HeartbeatResponse is returned by the server on heartbeat.
type HeartbeatResponse struct {
Success bool `json:"success"`
Upgrade *UpgradeSignal `json:"upgrade,omitempty"`
Success bool `json:"success"`
Upgrade *UpgradeSignal `json:"upgrade,omitempty"`
Watching bool `json:"watching,omitempty"` // true when a user is viewing download progress in the web UI
}
// UpgradeSignal tells the agent to upgrade to a specific version.
@ -122,14 +133,6 @@ type UpgradeSignal struct {
Version string `json:"version"`
}
// UpgradeResult is sent by the agent after an upgrade attempt.
type UpgradeResult struct {
AgentID string `json:"agentId"`
Success bool `json:"success"`
Version string `json:"version,omitempty"`
Error string `json:"error,omitempty"`
}
// ErrorResponse is returned on API errors.
type ErrorResponse struct {
Error string `json:"error"`