From cc90e71eb20b4d70539907209420c7daa403eea8 Mon Sep 17 00:00:00 2001 From: Manuel Dewald Date: Wed, 25 Feb 2026 14:20:03 +0100 Subject: [PATCH 1/2] feat: Improved handling of unexpected errors This happens e.g. when a wrong API endpoint is set and the error cannot be parsed by the OpenAPI client. In this case we want to at least output the requested URL and the response code. --- api/client.go | 36 ++++++++++----------- api/errors/errors.go | 19 ++++++++--- api/errors/errors_test.go | 20 +++++++++--- api/plan.go | 4 +-- api/team.go | 16 ++++----- api/workspace.go | 68 +++++++++++++++++++-------------------- cli/cmd/exec.go | 3 +- cli/cmd/git_pull.go | 2 +- 8 files changed, 95 insertions(+), 73 deletions(-) diff --git a/api/client.go b/api/client.go index c392e14..2dd261d 100644 --- a/api/client.go +++ b/api/client.go @@ -78,46 +78,46 @@ func NewHttpClient() *http.Client { } func (c *Client) ListDataCenters() ([]DataCenter, error) { - datacenters, _, err := c.api.MetadataAPI.MetadataGetDatacenters(c.ctx).Execute() - return datacenters, errors.FormatAPIError(err) + datacenters, r, err := c.api.MetadataAPI.MetadataGetDatacenters(c.ctx).Execute() + return datacenters, errors.FormatAPIError(r, err) } func (c *Client) ListDomains(teamId int) ([]Domain, error) { - domains, _, err := c.api.DomainsAPI.DomainsListDomains(c.ctx, float32(teamId)).Execute() - return domains, errors.FormatAPIError(err) + domains, r, err := c.api.DomainsAPI.DomainsListDomains(c.ctx, float32(teamId)).Execute() + return domains, errors.FormatAPIError(r, err) } func (c *Client) GetDomain(teamId int, domainName string) (*Domain, error) { - domain, _, err := c.api.DomainsAPI.DomainsGetDomain(c.ctx, float32(teamId), domainName).Execute() - return domain, errors.FormatAPIError(err) + domain, r, err := c.api.DomainsAPI.DomainsGetDomain(c.ctx, float32(teamId), domainName).Execute() + return domain, errors.FormatAPIError(r, err) } func (c *Client) CreateDomain(teamId int, domainName string) (*Domain, error) { - domain, _, err := c.api.DomainsAPI.DomainsCreateDomain(c.ctx, float32(teamId), domainName).Execute() - return domain, errors.FormatAPIError(err) + domain, r, err := c.api.DomainsAPI.DomainsCreateDomain(c.ctx, float32(teamId), domainName).Execute() + return domain, errors.FormatAPIError(r, err) } func (c *Client) DeleteDomain(teamId int, domainName string) error { - _, err := c.api.DomainsAPI.DomainsDeleteDomain(c.ctx, float32(teamId), domainName).Execute() - return errors.FormatAPIError(err) + r, err := c.api.DomainsAPI.DomainsDeleteDomain(c.ctx, float32(teamId), domainName).Execute() + return errors.FormatAPIError(r, err) } func (c *Client) UpdateDomain( teamId int, domainName string, args UpdateDomainArgs, ) (*Domain, error) { - domain, _, err := c.api.DomainsAPI. + domain, r, err := c.api.DomainsAPI. DomainsUpdateDomain(c.ctx, float32(teamId), domainName). DomainsUpdateDomainRequest(args). Execute() - return domain, errors.FormatAPIError(err) + return domain, errors.FormatAPIError(r, err) } func (c *Client) VerifyDomain( teamId int, domainName string, ) (*DomainVerificationStatus, error) { - status, _, err := c.api.DomainsAPI. + status, r, err := c.api.DomainsAPI. DomainsVerifyDomain(c.ctx, float32(teamId), domainName).Execute() - return status, errors.FormatAPIError(err) + return status, errors.FormatAPIError(r, err) } func (c *Client) UpdateWorkspaceConnections( @@ -131,13 +131,13 @@ func (c *Client) UpdateWorkspaceConnections( } req[path] = ids } - domain, _, err := c.api.DomainsAPI. + domain, r, err := c.api.DomainsAPI. DomainsUpdateWorkspaceConnections(c.ctx, float32(teamId), domainName). RequestBody(req).Execute() - return domain, errors.FormatAPIError(err) + return domain, errors.FormatAPIError(r, err) } func (c *Client) ListBaseimages() ([]Baseimage, error) { - baseimages, _, err := c.api.MetadataAPI.MetadataGetWorkspaceBaseImages(c.ctx).Execute() - return baseimages, errors.FormatAPIError(err) + baseimages, r, err := c.api.MetadataAPI.MetadataGetWorkspaceBaseImages(c.ctx).Execute() + return baseimages, errors.FormatAPIError(r, err) } diff --git a/api/errors/errors.go b/api/errors/errors.go index 0bd1892..c074074 100644 --- a/api/errors/errors.go +++ b/api/errors/errors.go @@ -6,6 +6,8 @@ package errors import ( "encoding/json" "fmt" + "net/http" + "net/url" "time" "github.com/codesphere-cloud/cs-go/api/openapi_client" @@ -60,24 +62,33 @@ type APIErrorResponse struct { TraceId string `json:"traceId"` } -func FormatAPIError(err error) error { +func FormatAPIError(r *http.Response, err error) error { if err == nil { return nil } + if r == nil { + r = &http.Response{ + StatusCode: -1, + } + } + if r.Request == nil { + r.Request = &http.Request{URL: &url.URL{}} + } + openAPIErr, ok := err.(*openapi_client.GenericOpenAPIError) if !ok { - return err + return fmt.Errorf("unexpected error %d at URL %s: %w", r.StatusCode, r.Request.URL, err) } body := openAPIErr.Body() if len(body) == 0 { - return err + return fmt.Errorf("unexpected error %d at URL %s: %w", r.StatusCode, r.Request.URL, err) } var apiErr APIErrorResponse if json.Unmarshal(body, &apiErr) != nil { - return err + return fmt.Errorf("unexpected error %d at URL %s: %w", r.StatusCode, r.Request.URL, err) } return fmt.Errorf("codesphere API returned error %d (%s): %s", apiErr.Status, apiErr.Title, apiErr.Detail) diff --git a/api/errors/errors_test.go b/api/errors/errors_test.go index eb6fcb3..bdc6773 100644 --- a/api/errors/errors_test.go +++ b/api/errors/errors_test.go @@ -5,6 +5,8 @@ package errors_test import ( "fmt" + "net/http" + "net/url" "reflect" "unsafe" @@ -29,20 +31,30 @@ func makeGenericOpenAPIError(body []byte, errStr string) error { } var _ = Describe("FormatAPIError", func() { + var ( + r *http.Response + ) + + BeforeEach(func() { + r = &http.Response{ + StatusCode: 123, + Request: &http.Request{URL: &url.URL{Scheme: "http", Host: "codesphere.com", Path: "/api/fake"}}, + } + }) It("returns nil for nil error", func() { - Expect(errors.FormatAPIError(nil)).To(BeNil()) + Expect(errors.FormatAPIError(nil, nil)).To(BeNil()) }) It("returns regular error unchanged", func() { err := fmt.Errorf("regular error") - res := errors.FormatAPIError(err) + res := errors.FormatAPIError(r, err) Expect(res).ToNot(BeNil()) - Expect(res.Error()).To(Equal("regular error")) + Expect(res.Error()).To(Equal(fmt.Sprintf("unexpected error %d at URL %s: %s", r.StatusCode, r.Request.URL.String(), err.Error()))) }) It("parses API JSON error and formats it", func() { apiErr := makeGenericOpenAPIError([]byte(`{"status":400,"title":"Workspace is not running","detail":"Workspace '796636' is not in a running state.","traceId":"svJDMa5"}`), "400 Bad Request") - res := errors.FormatAPIError(apiErr) + res := errors.FormatAPIError(r, apiErr) Expect(res).ToNot(BeNil()) Expect(res.Error()).To(Equal("API error 400 Workspace is not running: Workspace '796636' is not in a running state.")) }) diff --git a/api/plan.go b/api/plan.go index 9d8caf0..6cb744b 100644 --- a/api/plan.go +++ b/api/plan.go @@ -27,6 +27,6 @@ func (client *Client) PlanByName(name string) (WorkspacePlan, error) { } func (c *Client) ListWorkspacePlans() ([]WorkspacePlan, error) { - plans, _, err := c.api.MetadataAPI.MetadataGetWorkspacePlans(c.ctx).Execute() - return plans, cserrors.FormatAPIError(err) + plans, r, err := c.api.MetadataAPI.MetadataGetWorkspacePlans(c.ctx).Execute() + return plans, cserrors.FormatAPIError(r, err) } diff --git a/api/team.go b/api/team.go index 48b2b7b..2f259ca 100644 --- a/api/team.go +++ b/api/team.go @@ -40,26 +40,26 @@ func (client *Client) TeamIdByName(name string) (Team, error) { } func (c *Client) ListTeams() ([]Team, error) { - teams, _, err := c.api.TeamsAPI.TeamsListTeams(c.ctx).Execute() - return teams, cserrors.FormatAPIError(err) + teams, r, err := c.api.TeamsAPI.TeamsListTeams(c.ctx).Execute() + return teams, cserrors.FormatAPIError(r, err) } func (c *Client) GetTeam(teamId int) (*Team, error) { - team, _, err := c.api.TeamsAPI.TeamsGetTeam(c.ctx, float32(teamId)).Execute() - return ConvertToTeam(team), cserrors.FormatAPIError(err) + team, r, err := c.api.TeamsAPI.TeamsGetTeam(c.ctx, float32(teamId)).Execute() + return ConvertToTeam(team), cserrors.FormatAPIError(r, err) } func (c *Client) CreateTeam(name string, dc int) (*Team, error) { - team, _, err := c.api.TeamsAPI.TeamsCreateTeam(c.ctx). + team, r, err := c.api.TeamsAPI.TeamsCreateTeam(c.ctx). TeamsCreateTeamRequest(openapi_client.TeamsCreateTeamRequest{ Name: name, Dc: dc, }). Execute() - return ConvertToTeam(team), cserrors.FormatAPIError(err) + return ConvertToTeam(team), cserrors.FormatAPIError(r, err) } func (c *Client) DeleteTeam(teamId int) error { - _, err := c.api.TeamsAPI.TeamsDeleteTeam(c.ctx, float32(teamId)).Execute() - return cserrors.FormatAPIError(err) + r, err := c.api.TeamsAPI.TeamsDeleteTeam(c.ctx, float32(teamId)).Execute() + return cserrors.FormatAPIError(r, err) } diff --git a/api/workspace.go b/api/workspace.go index 2ec8ef1..3400139 100644 --- a/api/workspace.go +++ b/api/workspace.go @@ -13,32 +13,32 @@ import ( ) func (c *Client) ListWorkspaces(teamId int) ([]Workspace, error) { - workspaces, _, err := c.api.WorkspacesAPI.WorkspacesListWorkspaces(c.ctx, float32(teamId)).Execute() - return workspaces, errors.FormatAPIError(err) + workspaces, r, err := c.api.WorkspacesAPI.WorkspacesListWorkspaces(c.ctx, float32(teamId)).Execute() + return workspaces, errors.FormatAPIError(r, err) } func (c *Client) GetWorkspace(workspaceId int) (Workspace, error) { - workspace, _, err := c.api.WorkspacesAPI.WorkspacesGetWorkspace(c.ctx, float32(workspaceId)).Execute() + workspace, r, err := c.api.WorkspacesAPI.WorkspacesGetWorkspace(c.ctx, float32(workspaceId)).Execute() if workspace != nil { - return *workspace, errors.FormatAPIError(err) + return *workspace, errors.FormatAPIError(r, err) } - return Workspace{}, errors.FormatAPIError(err) + return Workspace{}, errors.FormatAPIError(r, err) } func (c *Client) DeleteWorkspace(workspaceId int) error { - _, err := c.api.WorkspacesAPI.WorkspacesDeleteWorkspace(c.ctx, float32(workspaceId)).Execute() - return errors.FormatAPIError(err) + r, err := c.api.WorkspacesAPI.WorkspacesDeleteWorkspace(c.ctx, float32(workspaceId)).Execute() + return errors.FormatAPIError(r, err) } func (c *Client) WorkspaceStatus(workspaceId int) (*WorkspaceStatus, error) { - status, _, err := c.api.WorkspacesAPI.WorkspacesGetWorkspaceStatus(c.ctx, float32(workspaceId)).Execute() - return status, errors.FormatAPIError(err) + status, r, err := c.api.WorkspacesAPI.WorkspacesGetWorkspaceStatus(c.ctx, float32(workspaceId)).Execute() + return status, errors.FormatAPIError(r, err) } func (c *Client) CreateWorkspace(args CreateWorkspaceArgs) (*Workspace, error) { - workspace, _, err := c.api.WorkspacesAPI.WorkspacesCreateWorkspace(c.ctx).WorkspacesCreateWorkspaceRequest(args).Execute() - return workspace, errors.FormatAPIError(err) + workspace, r, err := c.api.WorkspacesAPI.WorkspacesCreateWorkspace(c.ctx).WorkspacesCreateWorkspaceRequest(args).Execute() + return workspace, errors.FormatAPIError(r, err) } func (c *Client) SetEnvVarOnWorkspace(workspaceId int, envVars map[string]string) error { @@ -52,8 +52,8 @@ func (c *Client) SetEnvVarOnWorkspace(workspaceId int, envVars map[string]string req := c.api.WorkspacesAPI.WorkspacesSetEnvVar(c.ctx, float32(workspaceId)). WorkspacesCreateWorkspaceRequestEnvInner(vars) - _, err := c.api.WorkspacesAPI.WorkspacesSetEnvVarExecute(req) - return errors.FormatAPIError(err) + r, err := c.api.WorkspacesAPI.WorkspacesSetEnvVarExecute(req) + return errors.FormatAPIError(r, err) } func (c *Client) ExecCommand(workspaceId int, command string, workdir string, env map[string]string) (string, string, error) { @@ -69,43 +69,43 @@ func (c *Client) ExecCommand(workspaceId int, command string, workdir string, en } req := c.api.WorkspacesAPI.WorkspacesExecuteCommand(c.ctx, float32(workspaceId)).WorkspacesExecuteCommandRequest(cmd) - res, _, err := req.Execute() + res, r, err := req.Execute() if err != nil { - return "", "", errors.FormatAPIError(err) + return "", "", errors.FormatAPIError(r, err) } if res == nil { - return "", "", errors.FormatAPIError(err) + return "", "", errors.FormatAPIError(r, err) } - return res.Output, res.Error, errors.FormatAPIError(err) + return res.Output, res.Error, errors.FormatAPIError(r, err) } func (c *Client) DeployLandscape(wsId int, profile string) error { if profile == "ci.yml" || profile == "" { req := c.api.WorkspacesAPI.WorkspacesDeployLandscape(c.ctx, float32(wsId)) - _, err := req.Execute() - return errors.FormatAPIError(err) + r, err := req.Execute() + return errors.FormatAPIError(r, err) } req := c.api.WorkspacesAPI.WorkspacesDeployLandscape1(c.ctx, float32(wsId), profile) - _, err := req.Execute() - return errors.FormatAPIError(err) + r, err := req.Execute() + return errors.FormatAPIError(r, err) } func (c *Client) StartPipelineStage(wsId int, profile string, stage string) error { if profile == "ci.yml" || profile == "" { req := c.api.WorkspacesAPI.WorkspacesStartPipelineStage(c.ctx, float32(wsId), stage) - _, err := req.Execute() - return errors.FormatAPIError(err) + r, err := req.Execute() + return errors.FormatAPIError(r, err) } req := c.api.WorkspacesAPI.WorkspacesStartPipelineStage1(c.ctx, float32(wsId), stage, profile) - _, err := req.Execute() - return errors.FormatAPIError(err) + r, err := req.Execute() + return errors.FormatAPIError(r, err) } func (c *Client) GetPipelineState(wsId int, stage string) ([]PipelineStatus, error) { req := c.api.WorkspacesAPI.WorkspacesPipelineStatus(c.ctx, float32(wsId), stage) - res, _, err := req.Execute() - return res, errors.FormatAPIError(err) + res, r, err := req.Execute() + return res, errors.FormatAPIError(r, err) } // ScaleWorkspace sets the number of replicas for a workspace. @@ -115,8 +115,8 @@ func (c *Client) ScaleWorkspace(wsId int, replicas int) error { WorkspacesUpdateWorkspaceRequest(openapi_client.WorkspacesUpdateWorkspaceRequest{ Replicas: &replicas, }) - _, err := req.Execute() - return errors.FormatAPIError(err) + r, err := req.Execute() + return errors.FormatAPIError(r, err) } // Waits for a given workspace to be running. @@ -131,7 +131,7 @@ func (client *Client) WaitForWorkspaceRunning(workspace *Workspace, timeout time if err != nil { if client.time.Now().After(maxWaitTime) { - return errors.FormatAPIError(err) + return err } client.time.Sleep(delay) continue @@ -202,11 +202,11 @@ func (client Client) DeployWorkspace(args DeployWorkspaceArgs) (*Workspace, erro func (c Client) GitPull(workspaceId int, remote string, branch string) error { if remote == "" { req := c.api.WorkspacesAPI.WorkspacesGitPull(c.ctx, float32(workspaceId)) - _, err := req.Execute() - return errors.FormatAPIError(err) + r, err := req.Execute() + return errors.FormatAPIError(r, err) } req := c.api.WorkspacesAPI.WorkspacesGitPull2(c.ctx, float32(workspaceId), remote, branch) - _, err := req.Execute() - return errors.FormatAPIError(err) + r, err := req.Execute() + return errors.FormatAPIError(r, err) } diff --git a/cli/cmd/exec.go b/cli/cmd/exec.go index e8d0d83..3738486 100644 --- a/cli/cmd/exec.go +++ b/cli/cmd/exec.go @@ -9,7 +9,6 @@ import ( "os" "strings" - "github.com/codesphere-cloud/cs-go/api/errors" "github.com/codesphere-cloud/cs-go/pkg/cs" "github.com/codesphere-cloud/cs-go/pkg/io" @@ -81,5 +80,5 @@ func (c *ExecCmd) ExecCommand(client Client, command string) error { log.Println("STDERR:") fmt.Fprintln(os.Stderr, stderr) } - return errors.FormatAPIError(err) + return err } diff --git a/cli/cmd/git_pull.go b/cli/cmd/git_pull.go index fa94bd1..feaa565 100644 --- a/cli/cmd/git_pull.go +++ b/cli/cmd/git_pull.go @@ -51,7 +51,7 @@ func AddGitPullCmd(git *cobra.Command, opts GlobalOptions) { Long: io.Long(`Pull latest changes from the remote git repository. if specified, pulls a specific branch.`), - Example: io.FormatExampleCommands("pull", []io.Example{ + Example: io.FormatExampleCommands("git pull", []io.Example{ {Cmd: "", Desc: "Pull latest HEAD from current branch"}, {Cmd: "--remote origin --branch staging", Desc: "Pull branch staging from remote origin"}, }), From bd5fcb855c6f2209fe832b1e1d665940a51dee27 Mon Sep 17 00:00:00 2001 From: NautiluX <2600004+NautiluX@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:11:56 +0000 Subject: [PATCH 2/2] chore(docs): Auto-update docs and licenses Signed-off-by: NautiluX <2600004+NautiluX@users.noreply.github.com> --- NOTICE | 4 ++-- docs/cs_git_pull.md | 4 ++-- pkg/tmpl/NOTICE | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/NOTICE b/NOTICE index 584da72..c861e91 100644 --- a/NOTICE +++ b/NOTICE @@ -125,9 +125,9 @@ License URL: https://github.com/go-git/gcfg/blob/3a3c6141e376/LICENSE ---------- Module: github.com/go-git/go-billy/v5 -Version: v5.7.0 +Version: v5.8.0 License: Apache-2.0 -License URL: https://github.com/go-git/go-billy/blob/v5.7.0/LICENSE +License URL: https://github.com/go-git/go-billy/blob/v5.8.0/LICENSE ---------- Module: github.com/go-git/go-git/v5 diff --git a/docs/cs_git_pull.md b/docs/cs_git_pull.md index 909d9d7..e72c903 100644 --- a/docs/cs_git_pull.md +++ b/docs/cs_git_pull.md @@ -16,10 +16,10 @@ cs git pull [flags] ``` # Pull latest HEAD from current branch -$ cs pull +$ cs git pull # Pull branch staging from remote origin -$ cs pull --remote origin --branch staging +$ cs git pull --remote origin --branch staging ``` ### Options diff --git a/pkg/tmpl/NOTICE b/pkg/tmpl/NOTICE index 584da72..c861e91 100644 --- a/pkg/tmpl/NOTICE +++ b/pkg/tmpl/NOTICE @@ -125,9 +125,9 @@ License URL: https://github.com/go-git/gcfg/blob/3a3c6141e376/LICENSE ---------- Module: github.com/go-git/go-billy/v5 -Version: v5.7.0 +Version: v5.8.0 License: Apache-2.0 -License URL: https://github.com/go-git/go-billy/blob/v5.7.0/LICENSE +License URL: https://github.com/go-git/go-billy/blob/v5.8.0/LICENSE ---------- Module: github.com/go-git/go-git/v5