Skip to content
16 changes: 7 additions & 9 deletions cmd/harbor/root/artifact/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@
package artifact

import (
"fmt"

"github.com/goharbor/go-client/pkg/sdk/v2.0/client/artifact"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/errors"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
artifactViews "github.com/goharbor/harbor-cli/pkg/views/artifact/list"
Expand All @@ -44,11 +43,11 @@ Supports pagination, search queries, and sorting using flags.`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if opts.PageSize < 0 {
return fmt.Errorf("page size must be greater than or equal to 0")
return errors.New("Invalid page size", "page size must be greater than or equal to 0")
}

if opts.PageSize > 100 {
return fmt.Errorf("page size should be less than or equal to 100")
return errors.New("Invalid page size", "page size must be less than or equal to 100")
}
var err error
var artifacts artifact.ListArtifactsOK
Expand All @@ -57,26 +56,25 @@ Supports pagination, search queries, and sorting using flags.`,
if len(args) > 0 {
projectName, repoName, err = utils.ParseProjectRepo(args[0])
if err != nil {
return fmt.Errorf("failed to parse project/repo: %v", err)
return err
}
} else {
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
return err
}
repoName = prompt.GetRepoNameFromUser(projectName)
}

artifacts, err = api.ListArtifact(projectName, repoName, opts)

if err != nil {
return fmt.Errorf("failed to list artifacts: %v", err)
return err
}

FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(artifacts, FormatFlag)
if err != nil {
if err = utils.PrintFormat(artifacts, FormatFlag); err != nil {
return err
}
} else {
Expand Down
213 changes: 213 additions & 0 deletions pkg/errors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package errors

import (
"errors"
"fmt"
"strings"

"github.com/charmbracelet/lipgloss/tree"
"github.com/goharbor/harbor-cli/pkg/views"
)

var (
as = errors.As
)

type Frame struct {
Message string
Hints []string
}

type Error struct {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this package and think it is great. I wanted to note that it might be worth to document that we are shadowing the std errors package and explicitely telling people that they have to be careful about it (Like making a dedicated docs md for this).
A good way to let people do this difference could be this:

import (
    stderrors "errors"
    "github.com/goharbor/harbor-cli/pkg/errors"
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for supporting my proposal. Yeah, maintaining proper documentation for this package will help maintainers and contributors to refactor either the project or the package itself. I think adding that doc to the CONTRIBUTING.md will inform the new contributors at the first place, if they work on this package or try to handle errors.

Also yeah, this package wraps the std errors, enforces to use it rather for uniformity across the codebase. Thus we could instruct the contributors to use this package instead of the std. So there will be only,

import (
    "github.com/goharbor/harbor-cli/pkg/errors"
)

frames []Frame
cause error
}

func New(message string, hints ...string) *Error {
return &Error{
frames: []Frame{{Message: message, Hints: hints}},
}
}

func Newf(format string, args ...any) *Error {
return &Error{
frames: []Frame{{Message: fmt.Sprintf(format, args...)}},
}
}

func NewWithCause(cause error, message string, hints ...string) *Error {
return &Error{
cause: cause,
frames: []Frame{{Message: message, Hints: hints}},
}
}

func (e *Error) WithMessage(message string, hints ...string) *Error {
e.frames = append(e.frames, Frame{Message: message, Hints: hints})
return e
}

func (e *Error) Error() string {
if len(e.frames) == 0 {
return ""
}

var parts []string

rootFrame := e.frames[0]
parts = append(parts, views.ErrCauseStyle.Render(rootFrame.Message))

if e.cause != nil {
if code := parseHarborErrorCode(e.cause); code != "" {
parts = append(parts,
views.ErrTitleStyle.Render("Code: ")+views.ErrCauseStyle.Render(code),
)
}
}

if e.cause != nil {
causeText := e.cause.Error()
if he := isHarborError(e.cause); he != nil {
causeText = he.Message()
}

cause := views.ErrTitleStyle.Render("Cause: ")
causeText = views.ErrCauseStyle.Render(causeText)
causeTree := tree.Root(cause + causeText).
Enumerator(tree.RoundedEnumerator).
EnumeratorStyle(views.ErrEnumeratorStyle).
ItemStyle(views.ErrHintStyle)

if he := isHarborError(e.cause); he != nil {
for _, h := range he.Hints() {
causeTree.Child(h)
}
}
parts = append(parts, causeTree.String())
}

if len(rootFrame.Hints) > 0 {
hintsTree := tree.New().
Root("Hints:").
RootStyle(views.ErrTitleStyle).
Enumerator(tree.RoundedEnumerator).
EnumeratorStyle(views.ErrEnumeratorStyle).
ItemStyle(views.ErrHintStyle)

for _, h := range rootFrame.Hints {
hintsTree.Child(h)
}
parts = append(parts, hintsTree.String())
}

if len(e.frames) > 1 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if you could test whether the tree structure output breaks in pipeline environments. I do not know whether all terms or envs would support this or expect single line error messages. Maybe @bupd and @NucleoFusion have some ideas, too.

Copy link
Copy Markdown
Contributor Author

@vg006 vg006 Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tree output is planned to only be returned on non-verbose mode, where it's used by a real user. But for the CI and any other environment, the verbose mode of logging will be used I guess. So this tree won't be renderded, rather the error will be detailed by the logger as in the PR #722.

Even if CI or other envs use non-verbose mode, it will work I believe, because the ANSI sequences are valid UTF-8 encoded, won't disrupt tools like grep, rg parsing them, when piped the harbor output in their scripts.

For example, I tested the following in the local env to check compatibility with grep

package main

import (
	"fmt"

	"github.com/charmbracelet/lipgloss"
)

func main() {
	style := lipgloss.NewStyle().
		Bold(true).
		Foreground(lipgloss.Color("205")).
		Padding(1)
	output := style.Render("Hello, Pipeline!")
	fmt.Println(output)
}

Preview

ci

msgTree := tree.New().
Root("Messages:").
RootStyle(views.ErrTitleStyle).
Enumerator(tree.RoundedEnumerator).
EnumeratorStyle(views.ErrEnumeratorStyle).
ItemStyle(views.ErrTitleStyle)

for _, f := range e.frames[1:] {
msgWithHints := tree.Root(f.Message).
RootStyle(views.ErrTitleStyle).
Enumerator(tree.RoundedEnumerator).
EnumeratorStyle(views.ErrEnumeratorStyle).
ItemStyle(views.ErrHintStyle)
for _, h := range f.Hints {
msgWithHints.Child(h)
}
msgTree.Child(msgWithHints)
}
parts = append(parts, msgTree.String())
}

return strings.Join(parts, "\n")
}

func (e *Error) Message() string {
if len(e.frames) == 0 {
return ""
}
return e.frames[0].Message
}

func (e *Error) Errors() []string {
msgs := make([]string, len(e.frames))
for i, f := range e.frames {
msgs[i] = f.Message
}
return msgs
}

func (e *Error) Hints() []string {
var all []string
for _, f := range e.frames {
all = append(all, f.Hints...)
}
return all
}

func (e *Error) Frames() []Frame {
return e.frames
}

func (e *Error) Cause() error { return e.cause }

func (e *Error) Status() string {
if e.cause == nil {
return ""
}
return parseHarborErrorCode(e.cause)
}

func (e *Error) Unwrap() error { return e.cause }
func AsError(err error) *Error {
var e *Error
if errors.As(err, &e) {
return e
}
return &Error{
frames: []Frame{{Message: parseHarborErrorMsg(err)}},
cause: err,
}
}

func IsError(err error) bool {
var e *Error
return as(err, &e)
}

func Cause(err error) error {
if e := isHarborError(err); e != nil {
return e.Cause()
}
return nil
}

func Hints(err error) []string {
if e := isHarborError(err); e != nil {
return e.Hints()
}
return nil
}

func Status(err error) string {
if e := isHarborError(err); e != nil {
return e.Status()
}
return ""
}
Loading
Loading