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
3 changes: 3 additions & 0 deletions pkg/workflow/bundler_runtime_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,16 @@ var bundlerRuntimeLog = logger.New("workflow:bundler_runtime_validation")
// validateNoRuntimeMixing checks that all files being bundled are compatible with the target runtime mode
// This prevents mixing nodejs-only scripts (that use child_process) with github-script scripts
// Returns an error if incompatible runtime modes are detected
// Note: This function uses fail-fast error handling because runtime mode conflicts in dependencies
// need to be resolved one at a time, and showing multiple conflicting dependency chains would be confusing
func validateNoRuntimeMixing(mainScript string, sources map[string]string, targetMode RuntimeMode) error {
bundlerRuntimeLog.Printf("Validating runtime mode compatibility: target_mode=%s", targetMode)

// Track which files have been checked to avoid redundant checks
checked := make(map[string]bool)

// Recursively validate the main script and its dependencies
// This uses fail-fast error handling because runtime conflicts need sequential resolution
return validateRuntimeModeRecursive(mainScript, "", sources, targetMode, checked)
}

Expand Down
133 changes: 133 additions & 0 deletions pkg/workflow/dispatch_workflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,136 @@ No agentic-workflows tool is present.
assert.Contains(t, err.Error(), "nonexistent",
"Error should mention the workflow name")
}

// TestDispatchWorkflowMultipleErrors tests that multiple validation errors are aggregated
func TestDispatchWorkflowMultipleErrors(t *testing.T) {
compiler := NewCompilerWithVersion("1.0.0")
compiler.failFast = false // Enable error aggregation

tmpDir := t.TempDir()
awDir := filepath.Join(tmpDir, ".github", "aw")
workflowsDir := filepath.Join(tmpDir, ".github", "workflows")

err := os.MkdirAll(awDir, 0755)
require.NoError(t, err, "Failed to create aw directory")
err = os.MkdirAll(workflowsDir, 0755)
require.NoError(t, err, "Failed to create workflows directory")

// Create a workflow WITHOUT workflow_dispatch
ciWorkflow := `name: CI
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo "Running tests"
`
ciFile := filepath.Join(workflowsDir, "ci.lock.yml")
err = os.WriteFile(ciFile, []byte(ciWorkflow), 0644)
require.NoError(t, err, "Failed to write ci workflow")

// Create dispatcher workflow that references multiple problematic workflows
dispatcherWorkflow := `---
on: issues
engine: copilot
permissions:
contents: read
safe-outputs:
dispatch-workflow:
workflows:
- dispatcher # Self-reference
- ci # Missing workflow_dispatch
- nonexistent # Not found
max: 3
---

# Dispatcher Workflow

This workflow has multiple validation errors.
`
dispatcherFile := filepath.Join(awDir, "dispatcher.md")
err = os.WriteFile(dispatcherFile, []byte(dispatcherWorkflow), 0644)
require.NoError(t, err, "Failed to write dispatcher workflow")

// Change to the aw directory
oldDir, err := os.Getwd()
require.NoError(t, err, "Failed to get current directory")
err = os.Chdir(awDir)
require.NoError(t, err, "Failed to change directory")
defer func() { _ = os.Chdir(oldDir) }()

// Parse the dispatcher workflow
workflowData, err := compiler.ParseWorkflowFile("dispatcher.md")
require.NoError(t, err, "Failed to parse workflow")

// Validate the workflow - should return multiple errors
err = compiler.validateDispatchWorkflow(workflowData, dispatcherFile)
require.Error(t, err, "Validation should fail with multiple errors")

// Check that all three errors are present in the aggregated error
errStr := err.Error()
assert.Contains(t, errStr, "Found 3 dispatch-workflow errors:", "Should have error count header")
assert.Contains(t, errStr, "self-reference", "Should contain self-reference error")
assert.Contains(t, errStr, "dispatcher", "Should mention dispatcher workflow")
assert.Contains(t, errStr, "workflow_dispatch", "Should contain workflow_dispatch error")
assert.Contains(t, errStr, "ci", "Should mention ci workflow")
assert.Contains(t, errStr, "not found", "Should contain not found error")
assert.Contains(t, errStr, "nonexistent", "Should mention nonexistent workflow")
}

// TestDispatchWorkflowMultipleErrorsFailFast tests fail-fast mode stops at first error
func TestDispatchWorkflowMultipleErrorsFailFast(t *testing.T) {
compiler := NewCompilerWithVersion("1.0.0")
compiler.failFast = true // Enable fail-fast mode

tmpDir := t.TempDir()
awDir := filepath.Join(tmpDir, ".github", "aw")
workflowsDir := filepath.Join(tmpDir, ".github", "workflows")

err := os.MkdirAll(awDir, 0755)
require.NoError(t, err, "Failed to create aw directory")
err = os.MkdirAll(workflowsDir, 0755)
require.NoError(t, err, "Failed to create workflows directory")

// Create dispatcher workflow with multiple errors
dispatcherWorkflow := `---
on: issues
engine: copilot
permissions:
contents: read
safe-outputs:
dispatch-workflow:
workflows:
- dispatcher # Self-reference (first error)
- nonexistent # Not found (second error)
max: 2
---

# Dispatcher Workflow
`
dispatcherFile := filepath.Join(awDir, "dispatcher.md")
err = os.WriteFile(dispatcherFile, []byte(dispatcherWorkflow), 0644)
require.NoError(t, err, "Failed to write dispatcher workflow")

// Change to the aw directory
oldDir, err := os.Getwd()
require.NoError(t, err, "Failed to get current directory")
err = os.Chdir(awDir)
require.NoError(t, err, "Failed to change directory")
defer func() { _ = os.Chdir(oldDir) }()

// Parse the dispatcher workflow
workflowData, err := compiler.ParseWorkflowFile("dispatcher.md")
require.NoError(t, err, "Failed to parse workflow")

// Validate the workflow - should fail fast with first error only
err = compiler.validateDispatchWorkflow(workflowData, dispatcherFile)
require.Error(t, err, "Validation should fail")

// In fail-fast mode, only the first error should be returned
errStr := err.Error()
assert.Contains(t, errStr, "self-reference", "Should contain first error")
assert.NotContains(t, errStr, "Found 2", "Should not have multiple error header in fail-fast mode")
}
63 changes: 52 additions & 11 deletions pkg/workflow/dispatch_workflow_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,29 @@ func (c *Compiler) validateDispatchWorkflow(data *WorkflowData, workflowPath str
currentWorkflowName := getCurrentWorkflowName(workflowPath)
dispatchWorkflowValidationLog.Printf("Current workflow name: %s", currentWorkflowName)

// Collect all validation errors using ErrorCollector
collector := NewErrorCollector(c.failFast)

for _, workflowName := range config.Workflows {
dispatchWorkflowValidationLog.Printf("Validating workflow: %s", workflowName)

// Check for self-reference
if workflowName == currentWorkflowName {
return fmt.Errorf("dispatch-workflow: self-reference not allowed (workflow '%s' cannot dispatch itself)", workflowName)
selfRefErr := fmt.Errorf("dispatch-workflow: self-reference not allowed (workflow '%s' cannot dispatch itself)", workflowName)
if returnErr := collector.Add(selfRefErr); returnErr != nil {
return returnErr // Fail-fast mode
}
continue // Skip further validation for this workflow
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Multiple identical comments 'Skip further validation for this workflow' repeated throughout the function create noise. Consider reducing these comments or making them more specific to each validation step (e.g., 'Skip after self-reference check', 'Skip after file read error').

Copilot uses AI. Check for mistakes.
}

// Find the workflow file in multiple locations
fileResult, err := findWorkflowFile(workflowName, workflowPath)
if err != nil {
return fmt.Errorf("dispatch-workflow: error finding workflow '%s': %w", workflowName, err)
findErr := fmt.Errorf("dispatch-workflow: error finding workflow '%s': %w", workflowName, err)
if returnErr := collector.Add(findErr); returnErr != nil {
return returnErr // Fail-fast mode
}
continue // Skip further validation for this workflow
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Multiple identical comments 'Skip further validation for this workflow' repeated throughout the function create noise. Consider reducing these comments or making them more specific to each validation step (e.g., 'Skip after self-reference check', 'Skip after file read error').

Copilot uses AI. Check for mistakes.
}

// Check if any workflow file exists
Expand All @@ -53,8 +64,12 @@ func (c *Compiler) validateDispatchWorkflow(data *WorkflowData, workflowPath str
repoRoot := filepath.Dir(githubDir)
workflowsDir := filepath.Join(repoRoot, ".github", "workflows")

return fmt.Errorf("dispatch-workflow: workflow '%s' not found in %s (tried .md, .lock.yml, and .yml extensions)",
notFoundErr := fmt.Errorf("dispatch-workflow: workflow '%s' not found in %s (tried .md, .lock.yml, and .yml extensions)",
workflowName, workflowsDir)
if returnErr := collector.Add(notFoundErr); returnErr != nil {
return returnErr // Fail-fast mode
}
continue // Skip further validation for this workflow
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Multiple identical comments 'Skip further validation for this workflow' repeated throughout the function create noise. Consider reducing these comments or making them more specific to each validation step (e.g., 'Skip after self-reference check', 'Skip after file read error').

Copilot uses AI. Check for mistakes.
}

// Validate that the workflow supports workflow_dispatch
Expand All @@ -67,29 +82,49 @@ func (c *Compiler) validateDispatchWorkflow(data *WorkflowData, workflowPath str
workflowFile = fileResult.lockPath
workflowContent, readErr = os.ReadFile(fileResult.lockPath) // #nosec G304 -- Path is validated via isPathWithinDir in findWorkflowFile
if readErr != nil {
return fmt.Errorf("dispatch-workflow: failed to read workflow file %s: %w", fileResult.lockPath, readErr)
fileReadErr := fmt.Errorf("dispatch-workflow: failed to read workflow file %s: %w", fileResult.lockPath, readErr)
if returnErr := collector.Add(fileReadErr); returnErr != nil {
return returnErr // Fail-fast mode
}
continue // Skip further validation for this workflow
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Multiple identical comments 'Skip further validation for this workflow' repeated throughout the function create noise. Consider reducing these comments or making them more specific to each validation step (e.g., 'Skip after self-reference check', 'Skip after file read error').

Copilot uses AI. Check for mistakes.
}
} else if fileResult.ymlExists {
workflowFile = fileResult.ymlPath
workflowContent, readErr = os.ReadFile(fileResult.ymlPath) // #nosec G304 -- Path is validated via isPathWithinDir in findWorkflowFile
if readErr != nil {
return fmt.Errorf("dispatch-workflow: failed to read workflow file %s: %w", fileResult.ymlPath, readErr)
fileReadErr := fmt.Errorf("dispatch-workflow: failed to read workflow file %s: %w", fileResult.ymlPath, readErr)
if returnErr := collector.Add(fileReadErr); returnErr != nil {
return returnErr // Fail-fast mode
}
continue // Skip further validation for this workflow
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Multiple identical comments 'Skip further validation for this workflow' repeated throughout the function create noise. Consider reducing these comments or making them more specific to each validation step (e.g., 'Skip after self-reference check', 'Skip after file read error').

Copilot uses AI. Check for mistakes.
}
} else {
// Only .md exists - needs to be compiled first
return fmt.Errorf("dispatch-workflow: workflow '%s' must be compiled first (run 'gh aw compile %s')", workflowName, fileResult.mdPath)
compileErr := fmt.Errorf("dispatch-workflow: workflow '%s' must be compiled first (run 'gh aw compile %s')", workflowName, fileResult.mdPath)
if returnErr := collector.Add(compileErr); returnErr != nil {
return returnErr // Fail-fast mode
}
continue // Skip further validation for this workflow
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Multiple identical comments 'Skip further validation for this workflow' repeated throughout the function create noise. Consider reducing these comments or making them more specific to each validation step (e.g., 'Skip after self-reference check', 'Skip after file read error').

Copilot uses AI. Check for mistakes.
}

// Parse the workflow YAML to check for workflow_dispatch trigger
var workflow map[string]any
if err := yaml.Unmarshal(workflowContent, &workflow); err != nil {
return fmt.Errorf("dispatch-workflow: failed to parse workflow file %s: %w", workflowFile, err)
parseErr := fmt.Errorf("dispatch-workflow: failed to parse workflow file %s: %w", workflowFile, err)
if returnErr := collector.Add(parseErr); returnErr != nil {
return returnErr // Fail-fast mode
}
continue // Skip further validation for this workflow
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Multiple identical comments 'Skip further validation for this workflow' repeated throughout the function create noise. Consider reducing these comments or making them more specific to each validation step (e.g., 'Skip after self-reference check', 'Skip after file read error').

Copilot uses AI. Check for mistakes.
}

// Check if the workflow has an "on" section
onSection, hasOn := workflow["on"]
if !hasOn {
return fmt.Errorf("dispatch-workflow: workflow '%s' does not have an 'on' trigger section", workflowName)
onSectionErr := fmt.Errorf("dispatch-workflow: workflow '%s' does not have an 'on' trigger section", workflowName)
if returnErr := collector.Add(onSectionErr); returnErr != nil {
return returnErr // Fail-fast mode
}
continue // Skip further validation for this workflow
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Multiple identical comments 'Skip further validation for this workflow' repeated throughout the function create noise. Consider reducing these comments or making them more specific to each validation step (e.g., 'Skip after self-reference check', 'Skip after file read error').

Copilot uses AI. Check for mistakes.
}

// Check if workflow_dispatch is in the "on" section
Expand All @@ -114,14 +149,20 @@ func (c *Compiler) validateDispatchWorkflow(data *WorkflowData, workflowPath str
}

if !hasWorkflowDispatch {
return fmt.Errorf("dispatch-workflow: workflow '%s' does not support workflow_dispatch trigger (must include 'workflow_dispatch' in the 'on' section)", workflowName)
dispatchErr := fmt.Errorf("dispatch-workflow: workflow '%s' does not support workflow_dispatch trigger (must include 'workflow_dispatch' in the 'on' section)", workflowName)
if returnErr := collector.Add(dispatchErr); returnErr != nil {
return returnErr // Fail-fast mode
}
continue // Skip further validation for this workflow
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Multiple identical comments 'Skip further validation for this workflow' repeated throughout the function create noise. Consider reducing these comments or making them more specific to each validation step (e.g., 'Skip after self-reference check', 'Skip after file read error').

Copilot uses AI. Check for mistakes.
}

dispatchWorkflowValidationLog.Printf("Workflow '%s' is valid for dispatch (found in %s)", workflowName, workflowFile)
}

dispatchWorkflowValidationLog.Printf("All %d workflows validated successfully", len(config.Workflows))
return nil
dispatchWorkflowValidationLog.Printf("Dispatch workflow validation completed: error_count=%d, total_workflows=%d", collector.Count(), len(config.Workflows))

// Return aggregated errors with formatted output
return collector.FormattedError("dispatch-workflow")
}

// extractWorkflowDispatchInputs parses a workflow file and extracts the workflow_dispatch inputs schema
Expand Down
27 changes: 16 additions & 11 deletions pkg/workflow/repository_features_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ func (c *Compiler) validateRepositoryFeatures(workflowData *WorkflowData) error

repositoryFeaturesLog.Printf("Checking repository features for: %s", repo)

// Collect all validation errors using ErrorCollector
collector := NewErrorCollector(c.failFast)

// Check if discussions are enabled when create-discussion or add-comment with discussion: true is configured
needsDiscussions := workflowData.SafeOutputs.CreateDiscussions != nil ||
(workflowData.SafeOutputs.AddComments != nil &&
Expand All @@ -128,10 +131,8 @@ func (c *Compiler) validateRepositoryFeatures(workflowData *WorkflowData) error
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(
fmt.Sprintf("Could not verify if discussions are enabled: %v", err)))
}
return nil
}

if !hasDiscussions {
// Continue checking other features even if this check fails
} else if !hasDiscussions {
// Changed to warning instead of error per issue feedback
// Strategy: Always try to create the discussion at runtime and investigate if it fails
// The runtime create_discussion handler will provide better error messages if creation fails
Expand All @@ -146,7 +147,7 @@ func (c *Compiler) validateRepositoryFeatures(workflowData *WorkflowData) error
if c.verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(warningMsg))
}
// Don't return error - allow compilation to proceed and let runtime handle the actual attempt
// Don't add to error collector - this is a warning, not an error
}
}

Expand All @@ -161,15 +162,19 @@ func (c *Compiler) validateRepositoryFeatures(workflowData *WorkflowData) error
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(
fmt.Sprintf("Could not verify if issues are enabled: %v", err)))
}
return nil
}

if !hasIssues {
return fmt.Errorf("workflow uses safe-outputs.create-issue but repository %s does not have issues enabled. Enable issues in repository settings or remove create-issue from safe-outputs", repo)
// Continue to return aggregated errors even if this check fails
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Corrected spelling of 'to' to 'checking'; the comment should read 'Continue checking other features even if this check fails' to match the pattern established in line 134.

Suggested change
// Continue to return aggregated errors even if this check fails
// Continue checking other features even if this check fails

Copilot uses AI. Check for mistakes.
} else if !hasIssues {
issueErr := fmt.Errorf("workflow uses safe-outputs.create-issue but repository %s does not have issues enabled. Enable issues in repository settings or remove create-issue from safe-outputs", repo)
if returnErr := collector.Add(issueErr); returnErr != nil {
return returnErr // Fail-fast mode
}
}
}

return nil
repositoryFeaturesLog.Printf("Repository features validation completed: error_count=%d", collector.Count())

// Return aggregated errors with formatted output
return collector.FormattedError("repository features")
}

// getCurrentRepository gets the current repository from git context (with caching)
Expand Down
Loading
Loading