diff --git a/pkg/cli/commands.go b/pkg/cli/commands.go index d77708104e..8ecb48db41 100644 --- a/pkg/cli/commands.go +++ b/pkg/cli/commands.go @@ -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 } @@ -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 @@ -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 { diff --git a/pkg/cli/commands_compile_workflow_test.go b/pkg/cli/commands_compile_workflow_test.go index b9f6bd7617..fed88b2df1 100644 --- a/pkg/cli/commands_compile_workflow_test.go +++ b/pkg/cli/commands_compile_workflow_test.go @@ -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) + } + } + }) + } +} diff --git a/pkg/cli/templates/instructions.md b/pkg/cli/templates/instructions.md index 71c14786c4..7f7e2c5feb 100644 --- a/pkg/cli/templates/instructions.md +++ b/pkg/cli/templates/instructions.md @@ -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 `** - 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 ` 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 @@ -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. \ No newline at end of file +Use `gh aw compile --verbose` to see detailed validation messages, or `gh aw compile --verbose` to validate a specific workflow. \ No newline at end of file