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
25 changes: 14 additions & 11 deletions pkg/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,10 +544,16 @@ func CompileWorkflows(markdownFile string, verbose bool, engineOverride string,
}

if markdownFile != "" {
// Resolve workflow ID or file path to actual file path
resolvedFile, err := resolveWorkflowFile(markdownFile, verbose)
if err != nil {
return fmt.Errorf("failed to resolve workflow: %w", err)
}

if verbose {
fmt.Printf("Compiling %s\n", markdownFile)
fmt.Printf("Compiling %s\n", resolvedFile)
}
if err := compiler.CompileWorkflow(markdownFile); err != nil {
if err := compiler.CompileWorkflow(resolvedFile); err != nil {
return err
}

Expand Down Expand Up @@ -3028,6 +3034,12 @@ func resolveWorkflowFile(fileOrWorkflowName string, verbose bool) (string, error
fmt.Printf("Created temporary workflow file: %s\n", tmpFile.Name())
}

defer func() {
if err := os.Remove(tmpFile.Name()); err != nil && verbose {
fmt.Printf("Warning: Failed to clean up temporary file %s: %v\n", tmpFile.Name(), err)
}
}()

return tmpFile.Name(), nil
} else {
// It's a local file, return the source path
Expand Down Expand Up @@ -3056,15 +3068,6 @@ func RunWorkflowOnGitHub(workflowIdOrName string, verbose bool) error {
return fmt.Errorf("failed to resolve workflow: %w", err)
}

// Check if we created a temporary file that needs cleanup
if strings.HasPrefix(workflowFile, os.TempDir()) {
defer func() {
if err := os.Remove(workflowFile); err != nil && verbose {
fmt.Printf("Warning: Failed to clean up temporary file %s: %v\n", workflowFile, err)
}
}()
}

// Check if the workflow is runnable (has workflow_dispatch trigger)
runnable, err := IsRunnable(workflowFile)
if err != nil {
Expand Down
131 changes: 131 additions & 0 deletions pkg/cli/commands_compile_workflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,3 +399,134 @@ func TestStageGitAttributesIfChanged(t *testing.T) {
})
}
}

func TestCompileWorkflowsWithWorkflowID(t *testing.T) {
tests := []struct {
name string
workflowID string
setupWorkflow func(string) error
expectError bool
errorContains string
}{
{
name: "compile with workflow ID successfully resolves to .md file",
workflowID: "test-workflow",
setupWorkflow: func(tmpDir string) error {
workflowContent := `---
name: Test Workflow
on:
push:
branches: [main]
permissions:
contents: read
---

# Test Workflow

This is a test workflow for compilation.
`
workflowsDir := filepath.Join(tmpDir, ".github", "workflows")
err := os.MkdirAll(workflowsDir, 0755)
if err != nil {
return err
}

workflowFile := filepath.Join(workflowsDir, "test-workflow.md")
return os.WriteFile(workflowFile, []byte(workflowContent), 0644)
},
expectError: false,
},
{
name: "compile with nonexistent workflow ID returns error",
workflowID: "nonexistent",
setupWorkflow: func(tmpDir string) error {
// Create workflows directory but no file
workflowsDir := filepath.Join(tmpDir, ".github", "workflows")
return os.MkdirAll(workflowsDir, 0755)
},
expectError: true,
errorContains: "workflow 'nonexistent' not found",
},
{
name: "compile with full path still works (backward compatibility)",
workflowID: ".github/workflows/test-workflow.md",
setupWorkflow: func(tmpDir string) error {
workflowContent := `---
name: Test Workflow
on:
push:
branches: [main]
permissions:
contents: read
---

# Test Workflow

This is a test workflow for backward compatibility.
`
workflowsDir := filepath.Join(tmpDir, ".github", "workflows")
err := os.MkdirAll(workflowsDir, 0755)
if err != nil {
return err
}

workflowFile := filepath.Join(workflowsDir, "test-workflow.md")
return os.WriteFile(workflowFile, []byte(workflowContent), 0644)
},
expectError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create temporary directory
tmpDir := t.TempDir()

// Initialize git repository in tmp directory
if err := initTestGitRepo(tmpDir); err != nil {
t.Fatalf("Failed to initialize git repo: %v", err)
}

// Change to temporary directory
oldDir, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get current directory: %v", err)
}
defer func() {
if err := os.Chdir(oldDir); err != nil {
t.Errorf("Failed to restore directory: %v", err)
}
}()

if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp directory: %v", err)
}

// Setup workflow file
if err := tt.setupWorkflow(tmpDir); err != nil {
t.Fatalf("Failed to setup workflow: %v", err)
}

// Test CompileWorkflows function with workflow ID
err = CompileWorkflows(tt.workflowID, false, "", false, false, false, false)

if tt.expectError {
if err == nil {
t.Errorf("Expected error but got none")
} else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
t.Errorf("Expected error to contain '%s', but got: %v", tt.errorContains, err)
}
} else {
if err != nil {
t.Errorf("Expected no error but got: %v", err)
}

// Verify the lock file was created
expectedLockFile := filepath.Join(".github", "workflows", "test-workflow.lock.yml")
if _, err := os.Stat(expectedLockFile); os.IsNotExist(err) {
t.Errorf("Expected lock file %s to be created", expectedLockFile)
}
}
})
}
}
12 changes: 10 additions & 2 deletions pkg/cli/templates/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,13 +436,21 @@ Agentic workflows compile to GitHub Actions YAML:
- Tool configurations are processed
- GitHub Actions syntax is generated

### Compilation Commands

- **`gh aw compile`** - Compile all workflow files in `.github/workflows/`
- **`gh aw compile <workflow-id>`** - Compile a specific workflow by ID (filename without extension)
- Example: `gh aw compile issue-triage` compiles `issue-triage.md`
- Supports partial matching and fuzzy search for workflow names
- **`gh aw compile --verbose`** - Show detailed compilation and validation messages

## Best Practices

1. **Use descriptive workflow names** that clearly indicate purpose
2. **Set appropriate timeouts** to prevent runaway costs
3. **Include security notices** for workflows processing user content
4. **Use @include directives** for common patterns and security boilerplate
5. **Test with `gh aw compile`** before committing
5. **Test with `gh aw compile`** before committing (or `gh aw compile <workflow-id>` for specific workflows)
6. **Review generated `.lock.yml`** files before deploying
7. **Set `stop-time`** for cost-sensitive workflows
8. **Set `max-turns`** to limit chat iterations and prevent runaway loops
Expand All @@ -457,4 +465,4 @@ The workflow frontmatter is validated against JSON Schema during compilation. Co
- **Invalid enum values** - e.g., `engine` must be "claude" or "codex"
- **Missing required fields** - Some triggers require specific configuration

Use `gh aw compile --verbose` to see detailed validation messages.
Use `gh aw compile --verbose` to see detailed validation messages, or `gh aw compile <workflow-id> --verbose` to validate a specific workflow.