Skip to content
Closed
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
6 changes: 3 additions & 3 deletions pkg/workflow/agent_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ func (c *Compiler) validateAgentFile(workflowData *WorkflowData, markdownPath st
if _, err := os.Stat(fullAgentPath); err != nil {
if os.IsNotExist(err) {
return formatCompilerError(markdownPath, "error",
fmt.Sprintf("agent file '%s' does not exist. Ensure the file exists in the repository and is properly imported.", agentPath))
fmt.Sprintf("agent file '%s' does not exist. Ensure the file exists in the repository and is properly imported.", agentPath), nil)
}
// Other error (permissions, etc.)
return formatCompilerError(markdownPath, "error",
fmt.Sprintf("failed to access agent file '%s': %v", agentPath, err))
fmt.Sprintf("failed to access agent file '%s'", agentPath), err)
}

if c.verbose {
Expand Down Expand Up @@ -228,7 +228,7 @@ func (c *Compiler) validateWorkflowRunBranches(workflowData *WorkflowData, markd

if c.strictMode {
// In strict mode, this is an error
return formatCompilerError(markdownPath, "error", message)
return formatCompilerError(markdownPath, "error", message, nil)
}

// In normal mode, this is a warning
Expand Down
92 changes: 66 additions & 26 deletions pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package workflow

import (
_ "embed"
"errors"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -37,11 +36,48 @@ const (
//go:embed schemas/github-workflow.json
var githubWorkflowSchema string

// compilerError is a custom error type that formats errors with IDE-friendly multi-line output
// while preserving the error chain for errors.Is() and errors.As()
type compilerError struct {
formatted string
cause error
}

func (e *compilerError) Error() string {
if e.cause == nil {
return e.formatted
}
// Format with cause on separate line(s) for IDE-friendliness
var builder strings.Builder
builder.WriteString(e.formatted)
builder.WriteString("\n")

// Add each error in the chain on a new line with indentation
causeStr := e.cause.Error()
lines := strings.Split(causeStr, "\n")
for _, line := range lines {
if line != "" {
builder.WriteString(" ")
builder.WriteString(line)
builder.WriteString("\n")
}
}

// Remove trailing newline
result := builder.String()
return strings.TrimSuffix(result, "\n")
}

func (e *compilerError) Unwrap() error {
return e.cause
}

// formatCompilerError creates a formatted compiler error message
// filePath: the file path to include in the error (typically markdownPath or lockFile)
// errType: the error type ("error" or "warning")
// message: the error message text
func formatCompilerError(filePath string, errType string, message string) error {
// cause: optional error to wrap (use nil for validation errors that create new messages)
func formatCompilerError(filePath string, errType string, message string, cause error) error {
formattedErr := console.FormatError(console.CompilerError{
Position: console.ErrorPosition{
File: filePath,
Expand All @@ -51,7 +87,11 @@ func formatCompilerError(filePath string, errType string, message string) error
Type: errType,
Message: message,
})
return errors.New(formattedErr)
// Return custom error type that preserves chain and formats multi-line
return &compilerError{
formatted: strings.TrimSuffix(formattedErr, "\n"),
cause: cause,
}
}

// formatCompilerMessage creates a formatted compiler message string (for warnings printed to stderr)
Expand Down Expand Up @@ -84,8 +124,8 @@ func (c *Compiler) CompileWorkflow(markdownPath string) error {
// Already formatted, return as-is
return err
}
// Otherwise, create a basic formatted error
return formatCompilerError(markdownPath, "error", err.Error())
// Otherwise, create a basic formatted error (don't wrap - this creates user-facing errors)
return formatCompilerError(markdownPath, "error", err.Error(), nil)
}

return c.CompileWorkflowData(workflowData, markdownPath)
Expand All @@ -97,7 +137,7 @@ func (c *Compiler) validateWorkflowData(workflowData *WorkflowData, markdownPath
// Validate expression safety - check that all GitHub Actions expressions are in the allowed list
log.Printf("Validating expression safety")
if err := validateExpressionSafety(workflowData.MarkdownContent); err != nil {
return formatCompilerError(markdownPath, "error", err.Error())
return formatCompilerError(markdownPath, "error", "expression safety validation failed", err)
}

// Validate expressions in runtime-import files at compile time
Expand All @@ -107,13 +147,13 @@ func (c *Compiler) validateWorkflowData(workflowData *WorkflowData, markdownPath
githubDir := filepath.Dir(workflowDir) // .github
workspaceDir := filepath.Dir(githubDir) // repo root
if err := validateRuntimeImportFiles(workflowData.MarkdownContent, workspaceDir); err != nil {
return formatCompilerError(markdownPath, "error", err.Error())
return formatCompilerError(markdownPath, "error", "runtime-import validation failed", err)
}

// Validate feature flags
log.Printf("Validating feature flags")
if err := validateFeatures(workflowData); err != nil {
return formatCompilerError(markdownPath, "error", err.Error())
return formatCompilerError(markdownPath, "error", "feature flag validation failed", err)
}

// Check for action-mode feature flag override
Expand All @@ -122,7 +162,7 @@ func (c *Compiler) validateWorkflowData(workflowData *WorkflowData, markdownPath
if actionModeStr, ok := actionModeVal.(string); ok && actionModeStr != "" {
mode := ActionMode(actionModeStr)
if !mode.IsValid() {
return formatCompilerError(markdownPath, "error", fmt.Sprintf("invalid action-mode feature flag '%s'. Must be 'dev', 'release', or 'script'", actionModeStr))
return formatCompilerError(markdownPath, "error", fmt.Sprintf("invalid action-mode feature flag '%s'. Must be 'dev', 'release', or 'script'", actionModeStr), nil)
}
log.Printf("Overriding action mode from feature flag: %s", mode)
c.SetActionMode(mode)
Expand All @@ -133,7 +173,7 @@ func (c *Compiler) validateWorkflowData(workflowData *WorkflowData, markdownPath
// Validate dangerous permissions
log.Printf("Validating dangerous permissions")
if err := validateDangerousPermissions(workflowData); err != nil {
return formatCompilerError(markdownPath, "error", err.Error())
return formatCompilerError(markdownPath, "error", "dangerous permissions validation failed", err)
}

// Validate agent file exists if specified in engine config
Expand All @@ -145,25 +185,25 @@ func (c *Compiler) validateWorkflowData(workflowData *WorkflowData, markdownPath
// Validate sandbox configuration
log.Printf("Validating sandbox configuration")
if err := validateSandboxConfig(workflowData); err != nil {
return formatCompilerError(markdownPath, "error", err.Error())
return formatCompilerError(markdownPath, "error", "sandbox configuration validation failed", err)
}

// Validate safe-outputs target configuration
log.Printf("Validating safe-outputs target fields")
if err := validateSafeOutputsTarget(workflowData.SafeOutputs); err != nil {
return formatCompilerError(markdownPath, "error", err.Error())
return formatCompilerError(markdownPath, "error", "safe-outputs target validation failed", err)
}

// Validate safe-outputs allowed-domains configuration
log.Printf("Validating safe-outputs allowed-domains")
if err := c.validateSafeOutputsAllowedDomains(workflowData.SafeOutputs); err != nil {
return formatCompilerError(markdownPath, "error", err.Error())
return formatCompilerError(markdownPath, "error", "safe-outputs allowed-domains validation failed", err)
}

// Validate network allowed domains configuration
log.Printf("Validating network allowed domains")
if err := c.validateNetworkAllowedDomains(workflowData.NetworkPermissions); err != nil {
return formatCompilerError(markdownPath, "error", err.Error())
return formatCompilerError(markdownPath, "error", "network allowed-domains validation failed", err)
}

// Emit experimental warning for sandbox-runtime feature
Expand Down Expand Up @@ -207,7 +247,7 @@ func (c *Compiler) validateWorkflowData(workflowData *WorkflowData, markdownPath
if len(validationResult.MissingPermissions) > 0 {
if c.strictMode {
// In strict mode, missing permissions are errors
return formatCompilerError(markdownPath, "error", message)
return formatCompilerError(markdownPath, "error", message, nil)
} else {
// In non-strict mode, missing permissions are warnings
fmt.Fprintln(os.Stderr, formatCompilerMessage(markdownPath, "warning", message))
Expand All @@ -226,7 +266,7 @@ func (c *Compiler) validateWorkflowData(workflowData *WorkflowData, markdownPath

// Validate that all allowed tools have their toolsets enabled
if err := ValidateGitHubToolsAgainstToolsets(allowedTools, enabledToolsets); err != nil {
return formatCompilerError(markdownPath, "error", err.Error())
return formatCompilerError(markdownPath, "error", "GitHub tools validation failed", err)
}

// Print informational message if "projects" toolset is explicitly specified
Expand Down Expand Up @@ -258,14 +298,14 @@ func (c *Compiler) validateWorkflowData(workflowData *WorkflowData, markdownPath
message += "permissions:\n"
message += " actions: read"

return formatCompilerError(markdownPath, "error", message)
return formatCompilerError(markdownPath, "error", message, nil)
}
}

// Validate dispatch-workflow configuration (independent of agentic-workflows tool)
log.Print("Validating dispatch-workflow configuration")
if err := c.validateDispatchWorkflow(workflowData, markdownPath); err != nil {
return formatCompilerError(markdownPath, "error", fmt.Sprintf("dispatch-workflow validation failed: %v", err))
return formatCompilerError(markdownPath, "error", "dispatch-workflow validation failed", err)
}

return nil
Expand All @@ -277,15 +317,15 @@ func (c *Compiler) generateAndValidateYAML(workflowData *WorkflowData, markdownP
// Generate the YAML content
yamlContent, err := c.generateYAML(workflowData, markdownPath)
if err != nil {
return "", formatCompilerError(markdownPath, "error", fmt.Sprintf("failed to generate YAML: %v", err))
return "", formatCompilerError(markdownPath, "error", "failed to generate YAML", err)
}

// Always validate expression sizes - this is a hard limit from GitHub Actions (21KB)
// that cannot be bypassed, so we validate it unconditionally
log.Print("Validating expression sizes")
if err := c.validateExpressionSizes(yamlContent); err != nil {
// Store error first so we can write invalid YAML before returning
formattedErr := formatCompilerError(markdownPath, "error", fmt.Sprintf("expression size validation failed: %v", err))
formattedErr := formatCompilerError(markdownPath, "error", "expression size validation failed", err)
// Write the invalid YAML to a .invalid.yml file for inspection
invalidFile := strings.TrimSuffix(lockFile, ".lock.yml") + ".invalid.yml"
if writeErr := os.WriteFile(invalidFile, []byte(yamlContent), 0644); writeErr == nil {
Expand All @@ -298,7 +338,7 @@ func (c *Compiler) generateAndValidateYAML(workflowData *WorkflowData, markdownP
log.Print("Validating for template injection vulnerabilities")
if err := validateNoTemplateInjection(yamlContent); err != nil {
// Store error first so we can write invalid YAML before returning
formattedErr := formatCompilerError(markdownPath, "error", err.Error())
formattedErr := formatCompilerError(markdownPath, "error", "template injection validation failed", err)
// Write the invalid YAML to a .invalid.yml file for inspection
invalidFile := strings.TrimSuffix(lockFile, ".lock.yml") + ".invalid.yml"
if writeErr := os.WriteFile(invalidFile, []byte(yamlContent), 0644); writeErr == nil {
Expand All @@ -312,7 +352,7 @@ func (c *Compiler) generateAndValidateYAML(workflowData *WorkflowData, markdownP
log.Print("Validating workflow against GitHub Actions schema")
if err := c.validateGitHubActionsSchema(yamlContent); err != nil {
// Store error first so we can write invalid YAML before returning
formattedErr := formatCompilerError(markdownPath, "error", fmt.Sprintf("workflow schema validation failed: %v", err))
formattedErr := formatCompilerError(markdownPath, "error", "workflow schema validation failed", err)
// Write the invalid YAML to a .invalid.yml file for inspection
invalidFile := strings.TrimSuffix(lockFile, ".lock.yml") + ".invalid.yml"
if writeErr := os.WriteFile(invalidFile, []byte(yamlContent), 0644); writeErr == nil {
Expand All @@ -333,19 +373,19 @@ func (c *Compiler) generateAndValidateYAML(workflowData *WorkflowData, markdownP
// Validate runtime packages (npx, uv)
log.Print("Validating runtime packages")
if err := c.validateRuntimePackages(workflowData); err != nil {
return "", formatCompilerError(markdownPath, "error", fmt.Sprintf("runtime package validation failed: %v", err))
return "", formatCompilerError(markdownPath, "error", "runtime package validation failed", err)
}

// Validate firewall configuration (log-level enum)
log.Print("Validating firewall configuration")
if err := c.validateFirewallConfig(workflowData); err != nil {
return "", formatCompilerError(markdownPath, "error", fmt.Sprintf("firewall configuration validation failed: %v", err))
return "", formatCompilerError(markdownPath, "error", "firewall configuration validation failed", err)
}

// Validate repository features (discussions, issues)
log.Print("Validating repository features")
if err := c.validateRepositoryFeatures(workflowData); err != nil {
return "", formatCompilerError(markdownPath, "error", fmt.Sprintf("repository feature validation failed: %v", err))
return "", formatCompilerError(markdownPath, "error", "repository feature validation failed", err)
}
} else if c.verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Schema validation available but skipped (use SetSkipValidation(false) to enable)"))
Expand Down Expand Up @@ -377,7 +417,7 @@ func (c *Compiler) writeWorkflowOutput(lockFile, yamlContent string, markdownPat
// Only write if content has changed
if !contentUnchanged {
if err := os.WriteFile(lockFile, []byte(yamlContent), 0644); err != nil {
return formatCompilerError(lockFile, "error", fmt.Sprintf("failed to write lock file: %v", err))
return formatCompilerError(lockFile, "error", "failed to write lock file", err)
}
log.Print("Lock file written successfully")
}
Expand Down
Loading