fix: replace fmt.Errorf and errors.New with coreerr.E() from go-log

Replace all remaining fmt.Errorf and errors.New calls in production code
with coreerr.E() for consistent error handling with context information.
This improves error messages by including the function context where
errors occur.

Modified files:
- github.go: 6 fmt.Errorf calls
- generic_http.go: 5 fmt.Errorf calls
- updater.go: 6 fmt.Errorf calls
- service.go: 4 fmt.Errorf calls
- github_test.go: Updated test expectation for new error format

All tests pass with the new error format.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-16 21:34:16 +00:00
parent 06c7c45740
commit 3b328137db
6 changed files with 29 additions and 23 deletions

View file

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
coreerr "forge.lthn.ai/core/go-log"
) )
// GenericUpdateInfo holds the information from a latest.json file. // GenericUpdateInfo holds the information from a latest.json file.
@ -27,28 +29,28 @@ type GenericUpdateInfo struct {
func GetLatestUpdateFromURL(baseURL string) (*GenericUpdateInfo, error) { func GetLatestUpdateFromURL(baseURL string) (*GenericUpdateInfo, error) {
u, err := url.Parse(baseURL) u, err := url.Parse(baseURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid base URL: %w", err) return nil, coreerr.E("GetLatestUpdateFromURL", "invalid base URL", err)
} }
// Append latest.json to the path // Append latest.json to the path
u.Path += "/latest.json" u.Path += "/latest.json"
resp, err := http.Get(u.String()) resp, err := http.Get(u.String())
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch latest.json: %w", err) return nil, coreerr.E("GetLatestUpdateFromURL", "failed to fetch latest.json", err)
} }
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch latest.json: status code %d", resp.StatusCode) return nil, coreerr.E("GetLatestUpdateFromURL", fmt.Sprintf("failed to fetch latest.json: status code %d", resp.StatusCode), nil)
} }
var info GenericUpdateInfo var info GenericUpdateInfo
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return nil, fmt.Errorf("failed to parse latest.json: %w", err) return nil, coreerr.E("GetLatestUpdateFromURL", "failed to parse latest.json", err)
} }
if info.Version == "" || info.URL == "" { if info.Version == "" || info.URL == "" {
return nil, fmt.Errorf("invalid latest.json content: version or url is missing") return nil, coreerr.E("GetLatestUpdateFromURL", "invalid latest.json content: version or url is missing", nil)
} }
return &info, nil return &info, nil

View file

@ -9,6 +9,7 @@ import (
"runtime" "runtime"
"strings" "strings"
coreerr "forge.lthn.ai/core/go-log"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@ -97,7 +98,7 @@ func (g *githubClient) getPublicReposWithAPIURL(ctx context.Context, apiURL, use
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
_ = resp.Body.Close() _ = resp.Body.Close()
return nil, fmt.Errorf("failed to fetch repos: %s", resp.Status) return nil, coreerr.E("github.getPublicReposWithAPIURL", fmt.Sprintf("failed to fetch repos: %s", resp.Status), nil)
} }
var repos []Repo var repos []Repo
@ -155,7 +156,7 @@ func (g *githubClient) GetLatestRelease(ctx context.Context, owner, repo, channe
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch releases: %s", resp.Status) return nil, coreerr.E("github.GetLatestRelease", fmt.Sprintf("failed to fetch releases: %s", resp.Status), nil)
} }
var releases []Release var releases []Release
@ -210,7 +211,7 @@ func (g *githubClient) GetReleaseByPullRequest(ctx context.Context, owner, repo
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch releases: %s", resp.Status) return nil, coreerr.E("github.GetReleaseByPullRequest", fmt.Sprintf("failed to fetch releases: %s", resp.Status), nil)
} }
var releases []Release var releases []Release
@ -266,7 +267,7 @@ func (g *githubClient) GetReleaseByPullRequest(ctx context.Context, owner, repo
// fmt.Println(url) // "https://example.com/download/linux-amd64" (on a Linux AMD64 system) // fmt.Println(url) // "https://example.com/download/linux-amd64" (on a Linux AMD64 system)
func GetDownloadURL(release *Release, releaseURLFormat string) (string, error) { func GetDownloadURL(release *Release, releaseURLFormat string) (string, error) {
if release == nil { if release == nil {
return "", fmt.Errorf("no release provided") return "", coreerr.E("GetDownloadURL", "no release provided", nil)
} }
if releaseURLFormat != "" { if releaseURLFormat != "" {
@ -298,5 +299,5 @@ func GetDownloadURL(release *Release, releaseURLFormat string) (string, error) {
} }
} }
return "", fmt.Errorf("no suitable download asset found for %s/%s", osName, archName) return "", coreerr.E("GetDownloadURL", fmt.Sprintf("no suitable download asset found for %s/%s", osName, archName), nil)
} }

View file

@ -75,7 +75,7 @@ func TestGetPublicRepos_Error(t *testing.T) {
Request: &http.Request{Method: "GET", URL: u}, Request: &http.Request{Method: "GET", URL: u},
}, },
}) })
expectedErr := "failed to fetch repos: 404 Not Found" expectedErr := "github.getPublicReposWithAPIURL: failed to fetch repos: 404 Not Found"
client := &githubClient{} client := &githubClient{}
oldClient := NewAuthenticatedClient oldClient := NewAuthenticatedClient
@ -89,7 +89,7 @@ func TestGetPublicRepos_Error(t *testing.T) {
// Test user repos // Test user repos
_, err := client.getPublicReposWithAPIURL(context.Background(), "https://api.github.com", "testuser") _, err := client.getPublicReposWithAPIURL(context.Background(), "https://api.github.com", "testuser")
if err.Error() != expectedErr { if err.Error() != expectedErr {
t.Fatalf("getPublicReposWithAPIURL for user failed: %v", err) t.Fatalf("getPublicReposWithAPIURL for user failed: expected %q, got %q", expectedErr, err.Error())
} }
} }

2
go.mod
View file

@ -4,6 +4,7 @@ go 1.26.0
require ( require (
forge.lthn.ai/core/cli v0.3.5 forge.lthn.ai/core/cli v0.3.5
forge.lthn.ai/core/go-log v0.0.4
github.com/Snider/Borg v0.2.0 github.com/Snider/Borg v0.2.0
github.com/minio/selfupdate v0.6.0 github.com/minio/selfupdate v0.6.0
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
@ -17,7 +18,6 @@ require (
forge.lthn.ai/core/go v0.3.1 // indirect forge.lthn.ai/core/go v0.3.1 // indirect
forge.lthn.ai/core/go-i18n v0.1.5 // indirect forge.lthn.ai/core/go-i18n v0.1.5 // indirect
forge.lthn.ai/core/go-inference v0.1.4 // indirect forge.lthn.ai/core/go-inference v0.1.4 // indirect
forge.lthn.ai/core/go-log v0.0.4 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.4.0 // indirect github.com/ProtonMail/go-crypto v1.4.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect

View file

@ -8,6 +8,8 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"strings" "strings"
coreerr "forge.lthn.ai/core/go-log"
) )
// StartupCheckMode defines the updater's behavior on startup. // StartupCheckMode defines the updater's behavior on startup.
@ -63,7 +65,7 @@ func NewUpdateService(config UpdateServiceConfig) (*UpdateService, error) {
if isGitHub { if isGitHub {
owner, repo, err = ParseRepoURL(config.RepoURL) owner, repo, err = ParseRepoURL(config.RepoURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse GitHub repo URL: %w", err) return nil, coreerr.E("NewUpdateService", "failed to parse GitHub repo URL", err)
} }
} }
@ -95,7 +97,7 @@ func (s *UpdateService) startGitHubCheck() error {
case CheckAndUpdateOnStartup: case CheckAndUpdateOnStartup:
return CheckForUpdates(s.owner, s.repo, s.config.Channel, s.config.ForceSemVerPrefix, s.config.ReleaseURLFormat) return CheckForUpdates(s.owner, s.repo, s.config.Channel, s.config.ForceSemVerPrefix, s.config.ReleaseURLFormat)
default: default:
return fmt.Errorf("unknown startup check mode: %d", s.config.CheckOnStartup) return coreerr.E("startGitHubCheck", fmt.Sprintf("unknown startup check mode: %d", s.config.CheckOnStartup), nil)
} }
} }
@ -108,7 +110,7 @@ func (s *UpdateService) startHTTPCheck() error {
case CheckAndUpdateOnStartup: case CheckAndUpdateOnStartup:
return CheckForUpdatesHTTP(s.config.RepoURL) return CheckForUpdatesHTTP(s.config.RepoURL)
default: default:
return fmt.Errorf("unknown startup check mode: %d", s.config.CheckOnStartup) return coreerr.E("startHTTPCheck", fmt.Sprintf("unknown startup check mode: %d", s.config.CheckOnStartup), nil)
} }
} }
@ -121,7 +123,7 @@ func ParseRepoURL(repoURL string) (owner string, repo string, err error) {
} }
parts := strings.Split(strings.Trim(u.Path, "/"), "/") parts := strings.Split(strings.Trim(u.Path, "/"), "/")
if len(parts) < 2 { if len(parts) < 2 {
return "", "", fmt.Errorf("invalid repo URL path: %s", u.Path) return "", "", coreerr.E("ParseRepoURL", fmt.Sprintf("invalid repo URL path: %s", u.Path), nil)
} }
return parts[0], parts[1], nil return parts[0], parts[1], nil
} }

View file

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"strings" "strings"
coreerr "forge.lthn.ai/core/go-log"
"github.com/minio/selfupdate" "github.com/minio/selfupdate"
"golang.org/x/mod/semver" "golang.org/x/mod/semver"
) )
@ -44,9 +45,9 @@ var DoUpdate = func(url string) error {
err = selfupdate.Apply(resp.Body, selfupdate.Options{}) err = selfupdate.Apply(resp.Body, selfupdate.Options{})
if err != nil { if err != nil {
if rerr := selfupdate.RollbackError(err); rerr != nil { if rerr := selfupdate.RollbackError(err); rerr != nil {
return fmt.Errorf("failed to rollback from failed update: %v", rerr) return coreerr.E("DoUpdate", "failed to rollback from failed update", rerr)
} }
return fmt.Errorf("update failed: %v", err) return coreerr.E("DoUpdate", "update failed", err)
} }
fmt.Println("Update applied successfully.") fmt.Println("Update applied successfully.")
@ -62,7 +63,7 @@ var CheckForNewerVersion = func(owner, repo, channel string, forceSemVerPrefix b
release, err := client.GetLatestRelease(ctx, owner, repo, channel) release, err := client.GetLatestRelease(ctx, owner, repo, channel)
if err != nil { if err != nil {
return nil, false, fmt.Errorf("error fetching latest release: %w", err) return nil, false, coreerr.E("CheckForNewerVersion", "error fetching latest release", err)
} }
if release == nil { if release == nil {
@ -105,7 +106,7 @@ var CheckForUpdates = func(owner, repo, channel string, forceSemVerPrefix bool,
downloadURL, err := GetDownloadURL(release, releaseURLFormat) downloadURL, err := GetDownloadURL(release, releaseURLFormat)
if err != nil { if err != nil {
return fmt.Errorf("error getting download URL: %w", err) return coreerr.E("CheckForUpdates", "error getting download URL", err)
} }
return DoUpdate(downloadURL) return DoUpdate(downloadURL)
@ -158,7 +159,7 @@ var CheckForUpdatesByPullRequest = func(owner, repo string, prNumber int, releas
release, err := client.GetReleaseByPullRequest(ctx, owner, repo, prNumber) release, err := client.GetReleaseByPullRequest(ctx, owner, repo, prNumber)
if err != nil { if err != nil {
return fmt.Errorf("error fetching release for pull request: %w", err) return coreerr.E("CheckForUpdatesByPullRequest", "error fetching release for pull request", err)
} }
if release == nil { if release == nil {
@ -170,7 +171,7 @@ var CheckForUpdatesByPullRequest = func(owner, repo string, prNumber int, releas
downloadURL, err := GetDownloadURL(release, releaseURLFormat) downloadURL, err := GetDownloadURL(release, releaseURLFormat)
if err != nil { if err != nil {
return fmt.Errorf("error getting download URL: %w", err) return coreerr.E("CheckForUpdatesByPullRequest", "error getting download URL", err)
} }
return DoUpdate(downloadURL) return DoUpdate(downloadURL)