Skip to content

ErrorCollector.FormattedError loses error chain when aggregating multiple errors #27664

@github-actions

Description

@github-actions

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

  • errors.Is / errors.As works on the returned error in tests
  • Existing tests in pkg/workflow/error_aggregation_test.go still pass
  • go test ./pkg/workflow/... passes

Generated by Sergo - Serena Go Expert · ● 388.4K ·

  • expires on Apr 28, 2026, 8:35 PM UTC

Metadata

Metadata

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions