Problem
ErrorCollector.FormattedError in pkg/workflow/workflow_errors.go:242 handles the multi-error case by building a formatted string and wrapping it with fmt.Errorf("%s", ...). This silently discards the individual error chain, making it impossible for callers to use errors.Is or errors.As to inspect specific wrapped errors.
Compare with ErrorCollector.Error() (line 225) which correctly uses errors.Join(c.errors...), preserving the full chain. FormattedError is the preferred method (per its own docstring) yet is architecturally weaker.
FormattedError is called in at least 5 places:
pkg/workflow/call_workflow_validation.go:178
pkg/workflow/dispatch_workflow_validation.go:138
pkg/workflow/dispatch_repository_validation.go:100
pkg/workflow/repository_features_validation.go:155
pkg/workflow/strict_mode_validation.go:88
Current Code
// workflow_errors.go:254-261
var sb strings.Builder
fmt.Fprintf(&sb, "Found %d %s errors:", len(c.errors), category)
for _, err := range c.errors {
sb.WriteString("\n • ")
sb.WriteString(err.Error())
}
return fmt.Errorf("%s", sb.String()) // chain is lost
Recommendation
Use a custom joined error type that both preserves the chain via errors.Join and formats with the count header:
func (c *ErrorCollector) FormattedError(category string) error {
if len(c.errors) == 0 {
return nil
}
if len(c.errors) == 1 {
return c.errors[0]
}
joined := errors.Join(c.errors...)
return fmt.Errorf("Found %d %s errors:\n%w", len(c.errors), category, joined)
}
This preserves errors.Is/errors.As traversal while keeping the human-readable count header.
Severity
- High — silently breaks error introspection for callers; affects 5 validation paths used in production compilation
Validation
Generated by Sergo - Serena Go Expert · ● 388.4K · ◷
Problem
ErrorCollector.FormattedErrorinpkg/workflow/workflow_errors.go:242handles the multi-error case by building a formatted string and wrapping it withfmt.Errorf("%s", ...). This silently discards the individual error chain, making it impossible for callers to useerrors.Isorerrors.Asto inspect specific wrapped errors.Compare with
ErrorCollector.Error()(line 225) which correctly useserrors.Join(c.errors...), preserving the full chain.FormattedErroris the preferred method (per its own docstring) yet is architecturally weaker.FormattedErroris called in at least 5 places:pkg/workflow/call_workflow_validation.go:178pkg/workflow/dispatch_workflow_validation.go:138pkg/workflow/dispatch_repository_validation.go:100pkg/workflow/repository_features_validation.go:155pkg/workflow/strict_mode_validation.go:88Current Code
Recommendation
Use a custom joined error type that both preserves the chain via
errors.Joinand formats with the count header:This preserves
errors.Is/errors.Astraversal while keeping the human-readable count header.Severity
Validation
errors.Is/errors.Asworks on the returned error in testspkg/workflow/error_aggregation_test.gostill passgo test ./pkg/workflow/...passes