Files
gh-action-readme/internal/apperrors/errors.go
Ismo Vuorinen 7f80105ff5 feat: go 1.25.5, dependency updates, renamed internal/errors (#129)
* feat: rename internal/errors to internal/apperrors

* fix(tests): clear env values before using in tests

* feat: rename internal/errors to internal/apperrors

* chore(deps): update go and all dependencies

* chore: remove renovate from pre-commit, formatting

* chore: sonarcloud fixes

* feat: consolidate constants to appconstants/constants.go

* chore: sonarcloud fixes

* feat: simplification, deduplication, test utils

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: clean up

* fix: config discovery, const deduplication

* chore: fixes
2026-01-01 23:17:29 +02:00

196 lines
5.2 KiB
Go

// Package apperrors provides enhanced error types with contextual information and suggestions.
package apperrors
import (
"errors"
"fmt"
"strings"
"github.com/ivuorinen/gh-action-readme/appconstants"
)
// Sentinel errors for typed error checking.
var (
// ErrFileNotFound indicates a file was not found.
ErrFileNotFound = errors.New("file not found")
// ErrPermissionDenied indicates a permission error.
ErrPermissionDenied = errors.New("permission denied")
// ErrInvalidYAML indicates YAML parsing failed.
ErrInvalidYAML = errors.New("invalid YAML")
// ErrGitHubAPI indicates a GitHub API error.
ErrGitHubAPI = errors.New("GitHub API error")
// ErrConfiguration indicates a configuration error.
ErrConfiguration = errors.New("configuration error")
)
// ContextualError provides enhanced error information with actionable suggestions.
type ContextualError struct {
Code appconstants.ErrorCode
Err error
Context string
Suggestions []string
HelpURL string
Details map[string]string
}
// Error implements the error interface.
func (ce *ContextualError) Error() string {
var b strings.Builder
// Primary error message
if ce.Context != "" {
b.WriteString(fmt.Sprintf("%s: %v", ce.Context, ce.Err))
} else {
b.WriteString(ce.Err.Error())
}
// Add error code for reference
b.WriteString(fmt.Sprintf(" [%s]", ce.Code))
// Add details if available
if len(ce.Details) > 0 {
b.WriteString("\n\nDetails:")
for key, value := range ce.Details {
b.WriteString(fmt.Sprintf("\n %s: %s", key, value))
}
}
// Add suggestions
if len(ce.Suggestions) > 0 {
b.WriteString("\n\nSuggestions:")
for _, suggestion := range ce.Suggestions {
b.WriteString("\n • " + suggestion)
}
}
// Add help URL
if ce.HelpURL != "" {
b.WriteString("\n\nFor more help: " + ce.HelpURL)
}
return b.String()
}
// Unwrap returns the wrapped error.
func (ce *ContextualError) Unwrap() error {
return ce.Err
}
// Is implements errors.Is support.
func (ce *ContextualError) Is(target error) bool {
if target == nil {
return false
}
// Check if target is also a ContextualError with same code
if targetCE, ok := target.(*ContextualError); ok {
return ce.Code == targetCE.Code
}
// Check wrapped error
return errors.Is(ce.Err, target)
}
// New creates a new ContextualError with the given code and message.
func New(code appconstants.ErrorCode, message string) *ContextualError {
return &ContextualError{
Code: code,
Err: errors.New(message),
}
}
// Wrap wraps an existing error with contextual information.
func Wrap(err error, code appconstants.ErrorCode, context string) *ContextualError {
if err == nil {
return nil
}
// If already a ContextualError, preserve existing info by creating a copy
if ce, ok := err.(*ContextualError); ok {
// Create a copy to avoid mutating the original
errCopy := &ContextualError{
Code: ce.Code,
Err: ce.Err,
Context: ce.Context,
Suggestions: ce.Suggestions,
HelpURL: ce.HelpURL,
Details: make(map[string]string),
}
// Copy details map
for k, v := range ce.Details {
errCopy.Details[k] = v
}
// Only update if not already set
if errCopy.Code == appconstants.ErrCodeUnknown {
errCopy.Code = code
}
if errCopy.Context == "" {
errCopy.Context = context
}
return errCopy
}
return &ContextualError{
Code: code,
Err: err,
Context: context,
}
}
// WithSuggestions adds suggestions to a ContextualError.
func (ce *ContextualError) WithSuggestions(suggestions ...string) *ContextualError {
ce.Suggestions = append(ce.Suggestions, suggestions...)
return ce
}
// WithDetails adds detail key-value pairs to a ContextualError.
func (ce *ContextualError) WithDetails(details map[string]string) *ContextualError {
if ce.Details == nil {
ce.Details = make(map[string]string)
}
for k, v := range details {
ce.Details[k] = v
}
return ce
}
// WithHelpURL adds a help URL to a ContextualError.
func (ce *ContextualError) WithHelpURL(url string) *ContextualError {
ce.HelpURL = url
return ce
}
// GetHelpURL returns a help URL for the given error code.
func GetHelpURL(code appconstants.ErrorCode) string {
baseURL := "https://github.com/ivuorinen/gh-action-readme/blob/main/docs/troubleshooting.md"
anchors := map[appconstants.ErrorCode]string{
appconstants.ErrCodeFileNotFound: "#file-not-found",
appconstants.ErrCodePermission: "#permission-denied",
appconstants.ErrCodeInvalidYAML: "#invalid-yaml",
appconstants.ErrCodeInvalidAction: "#invalid-action-file",
appconstants.ErrCodeNoActionFiles: "#no-action-files",
appconstants.ErrCodeGitHubAPI: "#github-api-errors",
appconstants.ErrCodeGitHubRateLimit: "#rate-limit-exceeded",
appconstants.ErrCodeGitHubAuth: "#authentication-errors",
appconstants.ErrCodeConfiguration: "#configuration-errors",
appconstants.ErrCodeValidation: "#validation-errors",
appconstants.ErrCodeTemplateRender: "#template-errors",
appconstants.ErrCodeFileWrite: "#file-write-errors",
appconstants.ErrCodeDependencyAnalysis: "#dependency-analysis",
appconstants.ErrCodeCacheAccess: "#cache-errors",
}
if anchor, ok := anchors[code]; ok {
return baseURL + anchor
}
return baseURL
}