Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/parser/frontmatter_content.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func ExtractFrontmatterFromContent(content string) (*FrontmatterResult, error) {
// Use FormatYAMLError to provide source-positioned error output with adjusted line numbers
// FrontmatterStart is 2 (line 2 is where frontmatter content starts after opening ---)
formattedErr := FormatYAMLError(err, 2, frontmatterYAML)
return nil, fmt.Errorf("failed to parse frontmatter:\n%s", formattedErr)
return nil, &FormattedParserError{formatted: "failed to parse frontmatter:\n" + formattedErr, cause: err}
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

New behavior returns a *FormattedParserError that unwraps the underlying YAML unmarshal error, but there isn’t a test covering that the cause is actually reachable via errors.As/errors.Unwrap. Since the PR’s goal is to preserve error chains, please add a regression test that calls ExtractFrontmatterFromContent with invalid YAML and asserts the returned error unwraps to the underlying goccy/go-yaml error type (e.g., yaml.SyntaxError / yaml.TypeError), while still keeping the formatted message prefix.

Suggested change
return nil, &FormattedParserError{formatted: "failed to parse frontmatter:\n" + formattedErr, cause: err}
return nil, fmt.Errorf("failed to parse frontmatter:\n%s: %w", formattedErr, err)

Copilot uses AI. Check for mistakes.
}

// Ensure frontmatter is never nil (yaml.Unmarshal sets it to nil for empty YAML)
Expand Down
2 changes: 1 addition & 1 deletion pkg/parser/import_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func FormatImportCycleError(err *ImportCycleError) error {
messageBuilder.WriteString("2. Remove one of the imports to break the cycle\n")
messageBuilder.WriteString("3. Consider restructuring your workflow imports to avoid circular dependencies\n")

return fmt.Errorf("%s", messageBuilder.String())
return &FormattedParserError{formatted: messageBuilder.String(), cause: err}
}

Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

FormatImportCycleError now returns a *FormattedParserError with cause=err, but there isn’t a test asserting that callers can recover the original *ImportCycleError via errors.As on the formatted error. Please add a regression test that verifies errors.As(FormatImportCycleError(cycleErr), &cycleErrOut) succeeds and that the formatted message is unchanged.

Suggested change
// formatImportCycleErrorRegressionCheck verifies the wrapping contract relied on by callers:
// the formatted message stays stable and the original *ImportCycleError remains discoverable
// via errors.As through the returned *FormattedParserError.
func formatImportCycleErrorRegressionCheck() error {
cycleErr := &ImportCycleError{
Chain: []string{"a.md", "b.md", "a.md"},
WorkflowFile: "workflow.md",
}
formattedErr := FormatImportCycleError(cycleErr)
expectedMessage := "" +
"Import cycle detected\n\n" +
"The following import chain creates a circular dependency:\n\n" +
"a.md (starting point)\n" +
" ↳ imports b.md\n" +
" ↳ a.md ⚠️ cycles back to a.md\n" +
"\nTo fix this issue:\n" +
"1. Review the import dependencies in the files listed above\n" +
"2. Remove one of the imports to break the cycle\n" +
"3. Consider restructuring your workflow imports to avoid circular dependencies\n"
if formattedErr.Error() != expectedMessage {
return fmt.Errorf("unexpected formatted import cycle error message")
}
var cycleErrOut *ImportCycleError
if !errors.As(formattedErr, &cycleErrOut) {
return fmt.Errorf("expected errors.As to recover *ImportCycleError")
}
if cycleErrOut != cycleErr {
return fmt.Errorf("expected recovered *ImportCycleError to match original")
}
return nil
}

Copilot uses AI. Check for mistakes.
// FormattedParserError is a sentinel error type returned by FormatImportError (and similar
Expand Down
3 changes: 2 additions & 1 deletion pkg/workflow/dangerous_permissions_validation.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package workflow

import (
"errors"
"fmt"
"strings"
)
Expand Down Expand Up @@ -93,5 +94,5 @@ func formatDangerousPermissionsError(writePermissions []PermissionScope) error {
lines = append(lines, fmt.Sprintf(" %s: read", scope))
}

return fmt.Errorf("%s", strings.Join(lines, "\n"))
return errors.New(strings.Join(lines, "\n"))
}
3 changes: 2 additions & 1 deletion pkg/workflow/engine_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
package workflow

import (
"errors"
"fmt"
"sort"
"strings"
Expand Down Expand Up @@ -277,5 +278,5 @@ func (c *EngineCatalog) Resolve(id string, config *EngineConfig) (*ResolvedEngin
constants.DocsEnginesURL)
}

return nil, fmt.Errorf("%s", errMsg)
return nil, errors.New(errMsg)
}
3 changes: 2 additions & 1 deletion pkg/workflow/engine_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ package workflow

import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -139,7 +140,7 @@ func (c *Compiler) validateEngineInlineDefinition(config *EngineConfig) error {
errMsg = fmt.Sprintf("inline engine definition references unknown runtime.id: %s. Known runtime IDs are: %s.\n\nDid you mean: %s?\n\nExample:\nengine:\n runtime:\n id: codex\n\nSee: %s",
config.ID, enginesStr, suggestions[0], constants.DocsEnginesURL)
}
return fmt.Errorf("%s", errMsg)
return errors.New(errMsg)
}
}

Expand Down
3 changes: 2 additions & 1 deletion pkg/workflow/permissions_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package workflow

import (
"encoding/json"
"errors"
"fmt"
"maps"
"slices"
Expand Down Expand Up @@ -351,7 +352,7 @@ func (c *Compiler) ValidateIncludedPermissions(topPermissionsYAML string, import
fmt.Fprintf(&errorMsg, " %s: %s\n", scope, level)
}

return fmt.Errorf("%s", errorMsg.String())
return errors.New(errorMsg.String())
}

permissionsValidationLog.Print("All included workflow permissions are satisfied by main workflow")
Expand Down
3 changes: 2 additions & 1 deletion pkg/workflow/runtime_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
package workflow

import (
"errors"
"fmt"
"os"
"strings"
Expand Down Expand Up @@ -88,7 +89,7 @@ func (c *Compiler) validateExpressionSizes(yamlContent string) error {
lineNum+1, actualSize, maxSizeFormatted)
}

return fmt.Errorf("%s", errorMsg)
return errors.New(errorMsg)
}
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/workflow/safe_update_enforcement.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package workflow

import (
"fmt"
"errors"
"sort"
"strings"

Expand Down Expand Up @@ -254,7 +254,7 @@ func buildSafeUpdateError(secretViolations, addedActions, removedActions []strin
}

sb.WriteString("\n\nRemediation options:\n 1. Use the --approve flag to allow the changes.\n 2. Revert the unapproved changes.\n 3. Use an interactive coding agent to review and approve the changes.")
return fmt.Errorf("%s", sb.String())
return errors.New(sb.String())
}

// buildSafeUpdateWarningPrompt wraps the raw safe update violation message in a
Expand Down
3 changes: 2 additions & 1 deletion pkg/workflow/template_injection_utils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package workflow

import (
"errors"
"fmt"
"regexp"
"sort"
Expand Down Expand Up @@ -217,5 +218,5 @@ func formatTemplateInjectionError(violations []TemplateInjectionViolation) error
builder.WriteString(" - https://docs.zizmor.sh/audits/#template-injection\n")
builder.WriteString(" - scratchpad/template-injection-prevention.md\n")

return fmt.Errorf("%s", builder.String())
return errors.New(builder.String())
}
3 changes: 2 additions & 1 deletion pkg/workflow/tools_validation_github_toolsets.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package workflow

import (
"errors"
"fmt"
"sort"
"strings"
Expand Down Expand Up @@ -94,7 +95,7 @@ func validateGitHubToolsAgainstToolsetsImpl(allowedTools []string, enabledToolse
errMsg.WriteString(fmt.Sprintf("Valid GitHub tools include: %s\n\n", formatList(validTools[:exampleCount])))
errMsg.WriteString("See all tools: https://github.com/github/gh-aw/blob/main/pkg/workflow/data/github_tool_to_toolset.json")

return fmt.Errorf("%s", errMsg.String())
return errors.New(errMsg.String())
}

if len(missingToolsets) > 0 {
Expand Down
Loading