fa4a48d5e7
* chore: remove orphan executor tracing file and deprecated ResolveScript wrappers - Delete internal/agentctl/tracing/executor.go (7 TraceExecutor* funcs) - Delete internal/orchestrator/executor/script_template.go (deprecated wrapper) - Delete scriptengine.ResolveScript convenience function Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove orphan setter methods across healthpoll, jira, linear, slack, updates - Delete Poller.SetInterval from integrations/healthpoll (no callers) - Delete Poller.SetIssueTickHook from jira and linear (no callers) - Delete Trigger.SetInterval and Trigger.SetMatchHook from slack (no callers) - Delete Service.SetPollInterval from system/updates (no callers) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove orphan wrapper functions and accessor method - Delete updates.FetchLatestRelease shim (wraps FetchLatestReleaseFrom, no callers) - Delete usage.NewClaudeUsageClient and NewCodexUsageClient shims (no callers) - Delete database.Service.Path() accessor (no callers) - Update ErrGitHubRateLimited godoc to reference FetchLatestReleaseFrom Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove unwired handleListExecutors MCP handler The sibling profile handlers (list/create/update/delete) are registered via RegisterFunc in handlers.go. handleListExecutors never was — dead flow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove stale hook-field comments after setter deletion SetIssueTickHook (jira, linear) and SetMatchHook (slack) were removed in the deadcode cleanup. Their struct fields still claimed "tests use this" — no test or caller can set these fields now. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove orphan hook fields and fire methods after setter deletion issueTickHook (jira/linear) and matchHook (slack) are unexported and never set after their public setters were removed; fireIssueTickHook / fireHook were permanent no-ops. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
75 lines
2.8 KiB
Go
75 lines
2.8 KiB
Go
package updates
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// defaultClientTimeout caps the GitHub poll when the caller passes a nil
|
|
// http.Client. The 6h interval already provides plenty of slack; an
|
|
// unbounded timeout could hang a goroutine for hours on a stalled socket.
|
|
const defaultClientTimeout = 30 * time.Second
|
|
|
|
// DefaultReleaseURL is the GitHub Releases API endpoint polled by the updates
|
|
// service. Exposed as a package-level variable so tests can point a stub
|
|
// server at the github client without surgery on Service.
|
|
var DefaultReleaseURL = "https://api.github.com/repos/kdlbs/kandev/releases/latest"
|
|
|
|
// ErrGitHubRateLimited is returned by FetchLatestReleaseFrom when GitHub
|
|
// responds 403 with X-RateLimit-Remaining: 0. Callers should log at info
|
|
// (not warn) and back off without burning further requests.
|
|
var ErrGitHubRateLimited = errors.New("github api rate limited")
|
|
|
|
type releasePayload struct {
|
|
TagName string `json:"tag_name"`
|
|
HTMLURL string `json:"html_url"`
|
|
}
|
|
|
|
// FetchLatestReleaseFrom performs an unauthenticated GET against the GitHub
|
|
// Releases API and returns the tag name + release URL. On non-2xx it returns
|
|
// an error including the HTTP status; on rate-limit responses it returns
|
|
// ErrGitHubRateLimited so callers can branch on it.
|
|
func FetchLatestReleaseFrom(ctx context.Context, client *http.Client, url string) (string, string, error) {
|
|
if client == nil {
|
|
client = &http.Client{Timeout: defaultClientTimeout}
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("build github request: %w", err)
|
|
}
|
|
req.Header.Set("Accept", "application/vnd.github+json")
|
|
req.Header.Set("User-Agent", "kandev-updates-poller")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("github request: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// GitHub returns 403 with X-RateLimit-Remaining: 0 for the primary unauth
|
|
// rate limit, and 429 for secondary/abuse-detection limits — both should
|
|
// surface as ErrGitHubRateLimited so the poller backs off identically.
|
|
if resp.StatusCode == http.StatusTooManyRequests ||
|
|
(resp.StatusCode == http.StatusForbidden && resp.Header.Get("X-RateLimit-Remaining") == "0") {
|
|
return "", "", ErrGitHubRateLimited
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
|
return "", "", fmt.Errorf("github status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var payload releasePayload
|
|
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
|
return "", "", fmt.Errorf("decode github response: %w", err)
|
|
}
|
|
if payload.TagName == "" {
|
|
return "", "", errors.New("github response missing tag_name")
|
|
}
|
|
return payload.TagName, payload.HTMLURL, nil
|
|
}
|