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
5 changes: 4 additions & 1 deletion cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,19 +206,21 @@ Examples:
` + constants.CLIExtensionPrefix + ` compile weekly-research # Compile a specific workflow
` + constants.CLIExtensionPrefix + ` compile weekly-research daily-plan # Compile multiple workflows
` + constants.CLIExtensionPrefix + ` compile workflow.md # Compile by file path
` + constants.CLIExtensionPrefix + ` compile --workflow-dir custom/workflows # Compile from custom directory
` + constants.CLIExtensionPrefix + ` compile --watch weekly-research # Watch and auto-compile`,
Run: func(cmd *cobra.Command, args []string) {
engineOverride, _ := cmd.Flags().GetString("engine")
validate, _ := cmd.Flags().GetBool("validate")
watch, _ := cmd.Flags().GetBool("watch")
workflowDir, _ := cmd.Flags().GetString("workflow-dir")
instructions, _ := cmd.Flags().GetBool("instructions")
noEmit, _ := cmd.Flags().GetBool("no-emit")
purge, _ := cmd.Flags().GetBool("purge")
if err := validateEngine(engineOverride); err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
os.Exit(1)
}
if err := cli.CompileWorkflows(args, verbose, engineOverride, validate, watch, instructions, noEmit, purge); err != nil {
if err := cli.CompileWorkflows(args, verbose, engineOverride, validate, watch, workflowDir, instructions, noEmit, purge); err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
os.Exit(1)
}
Expand Down Expand Up @@ -347,6 +349,7 @@ func init() {
compileCmd.Flags().StringP("engine", "a", "", "Override AI engine (claude, codex)")
compileCmd.Flags().Bool("validate", true, "Enable GitHub Actions workflow schema validation (default: true)")
compileCmd.Flags().BoolP("watch", "w", false, "Watch for changes to workflow files and recompile automatically")
compileCmd.Flags().String("workflow-dir", "", "Relative directory containing workflows (default: .github/workflows)")
compileCmd.Flags().Bool("instructions", false, "Generate or update GitHub Copilot instructions file")
compileCmd.Flags().Bool("no-emit", false, "Validate workflow without generating lock files")
compileCmd.Flags().Bool("purge", false, "Delete .lock.yml files that were not regenerated during compilation (only when no specific files are specified)")
Expand Down
20 changes: 16 additions & 4 deletions pkg/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -603,12 +603,24 @@ func CompileWorkflowWithValidation(compiler *workflow.Compiler, filePath string,

return nil
}
func CompileWorkflows(markdownFiles []string, verbose bool, engineOverride string, validate bool, watch bool, writeInstructions bool, noEmit bool, purge bool) error {
func CompileWorkflows(markdownFiles []string, verbose bool, engineOverride string, validate bool, watch bool, workflowDir string, writeInstructions bool, noEmit bool, purge bool) error {
// Validate purge flag usage
if purge && len(markdownFiles) > 0 {
return fmt.Errorf("--purge flag can only be used when compiling all markdown files (no specific files specified)")
}

// Validate and set default for workflow directory
if workflowDir == "" {
workflowDir = ".github/workflows"
} else {
// Ensure the path is relative
if filepath.IsAbs(workflowDir) {
return fmt.Errorf("workflow-dir must be a relative path, got: %s", workflowDir)
}
// Clean the path to avoid issues with ".." or other problematic elements
workflowDir = filepath.Clean(workflowDir)
}

// Create compiler with verbose flag and AI engine override
compiler := workflow.NewCompiler(verbose, engineOverride, GetVersion())

Expand Down Expand Up @@ -684,10 +696,10 @@ func CompileWorkflows(markdownFiles []string, verbose bool, engineOverride strin
return fmt.Errorf("compile without arguments requires being in a git repository: %w", err)
}

// Compile all markdown files in .github/workflows relative to git root
workflowsDir := filepath.Join(gitRoot, ".github/workflows")
// Compile all markdown files in the specified workflow directory relative to git root
workflowsDir := filepath.Join(gitRoot, workflowDir)
if _, err := os.Stat(workflowsDir); os.IsNotExist(err) {
return fmt.Errorf("the .github/workflows directory does not exist in git root (%s)", gitRoot)
return fmt.Errorf("the %s directory does not exist in git root (%s)", workflowDir, gitRoot)
}

if verbose {
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/commands_compile_workflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ This is a test workflow for backward compatibility.
if tt.workflowID != "" {
args = []string{tt.workflowID}
}
err = CompileWorkflows(args, false, "", false, false, false, false, false)
err = CompileWorkflows(args, false, "", false, false, "", false, false, false)

if tt.expectError {
if err == nil {
Expand Down
26 changes: 13 additions & 13 deletions pkg/cli/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func TestCompileWorkflows(t *testing.T) {
if tt.markdownFile != "" {
args = []string{tt.markdownFile}
}
err := CompileWorkflows(args, false, "", false, false, false, false, false)
err := CompileWorkflows(args, false, "", false, false, "", false, false, false)

if tt.expectError && err == nil {
t.Errorf("Expected error for test '%s', got nil", tt.name)
Expand All @@ -122,7 +122,7 @@ func TestCompileWorkflows(t *testing.T) {
func TestCompileWorkflowsPurgeFlag(t *testing.T) {
t.Run("purge flag validation with specific files", func(t *testing.T) {
// Test that purge flag is rejected when specific files are provided
err := CompileWorkflows([]string{"test.md"}, false, "", false, false, false, false, true)
err := CompileWorkflows([]string{"test.md"}, false, "", false, false, "", false, false, true)

if err == nil {
t.Error("Expected error when using --purge with specific files, got nil")
Expand Down Expand Up @@ -151,7 +151,7 @@ func TestCompileWorkflowsPurgeFlag(t *testing.T) {
// Test should not error when no specific files are provided with purge flag
// Note: This will still error because there are no .md files, but it shouldn't
// error specifically because of the purge flag validation
err := CompileWorkflows([]string{}, false, "", false, false, false, false, true)
err := CompileWorkflows([]string{}, false, "", false, false, "", false, false, true)

if err != nil {
// The error should NOT be about purge flag validation
Expand Down Expand Up @@ -189,7 +189,7 @@ This is a test workflow to verify the --no-emit flag functionality.`
}

// Test compilation with noEmit = false (should create lock file)
err = CompileWorkflows([]string{"no-emit-test"}, false, "", false, false, false, false, false)
err = CompileWorkflows([]string{"no-emit-test"}, false, "", false, false, "", false, false, false)
if err != nil {
t.Errorf("CompileWorkflows with noEmit=false should not error, got: %v", err)
}
Expand All @@ -203,7 +203,7 @@ This is a test workflow to verify the --no-emit flag functionality.`
os.Remove(".github/workflows/no-emit-test.lock.yml")

// Test compilation with noEmit = true (should NOT create lock file)
err = CompileWorkflows([]string{"no-emit-test"}, false, "", false, false, false, true, false)
err = CompileWorkflows([]string{"no-emit-test"}, false, "", false, false, "", false, true, false)
if err != nil {
t.Errorf("CompileWorkflows with noEmit=true should not error, got: %v", err)
}
Expand Down Expand Up @@ -333,14 +333,14 @@ func TestAllCommandsExist(t *testing.T) {
name string
}{
{func() error { return ListWorkflows(false) }, false, "ListWorkflows"},
{func() error { return AddWorkflowWithTracking("", 1, false, "", "", false, nil) }, false, "AddWorkflowWithTracking (empty name)"}, // Shows help when empty, doesn't error
{func() error { return CompileWorkflows([]string{}, false, "", false, false, false, false, false) }, false, "CompileWorkflows"}, // Should compile existing markdown files successfully
{func() error { return RemoveWorkflows("test", false) }, false, "RemoveWorkflows"}, // Should handle missing directory gracefully
{func() error { return StatusWorkflows("test", false) }, false, "StatusWorkflows"}, // Should handle missing directory gracefully
{func() error { return EnableWorkflows("test") }, false, "EnableWorkflows"}, // Should handle missing directory gracefully
{func() error { return DisableWorkflows("test") }, false, "DisableWorkflows"}, // Should handle missing directory gracefully
{func() error { return RunWorkflowOnGitHub("", false) }, true, "RunWorkflowOnGitHub"}, // Should error with empty workflow name
{func() error { return RunWorkflowsOnGitHub([]string{}, 0, false) }, true, "RunWorkflowsOnGitHub"}, // Should error with empty workflow list
{func() error { return AddWorkflowWithTracking("", 1, false, "", "", false, nil) }, false, "AddWorkflowWithTracking (empty name)"}, // Shows help when empty, doesn't error
{func() error { return CompileWorkflows([]string{}, false, "", false, false, "", false, false, false) }, false, "CompileWorkflows"}, // Should compile existing markdown files successfully
{func() error { return RemoveWorkflows("test", false) }, false, "RemoveWorkflows"}, // Should handle missing directory gracefully
{func() error { return StatusWorkflows("test", false) }, false, "StatusWorkflows"}, // Should handle missing directory gracefully
{func() error { return EnableWorkflows("test") }, false, "EnableWorkflows"}, // Should handle missing directory gracefully
{func() error { return DisableWorkflows("test") }, false, "DisableWorkflows"}, // Should handle missing directory gracefully
{func() error { return RunWorkflowOnGitHub("", false) }, true, "RunWorkflowOnGitHub"}, // Should error with empty workflow name
{func() error { return RunWorkflowsOnGitHub([]string{}, 0, false) }, true, "RunWorkflowsOnGitHub"}, // Should error with empty workflow list
}

for _, test := range tests {
Expand Down
201 changes: 201 additions & 0 deletions pkg/cli/workflow_dir_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package cli

import (
"os"
"os/exec"
"path/filepath"
"testing"
)

// TestCompileWorkflowsWithCustomWorkflowDir tests the --workflow-dir flag functionality
func TestCompileWorkflowsWithCustomWorkflowDir(t *testing.T) {
// Save current directory and defer restoration
originalWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get current working directory: %v", err)
}
defer func() {
_ = os.Chdir(originalWd)
}()

// Create a temporary git repository with custom workflow directory
tmpDir, err := os.MkdirTemp("", "workflow-dir-test")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tmpDir)

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

// Initialize git repository properly
cmd := exec.Command("git", "init")
cmd.Dir = tmpDir
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to initialize git repository: %v", err)
}

// Create custom workflow directory
customDir := "my-workflows"
if err := os.MkdirAll(customDir, 0755); err != nil {
t.Fatalf("Failed to create custom workflow directory: %v", err)
}

// Create a test workflow file
workflowContent := `---
on: push
---

# Test Workflow

This is a test workflow in a custom directory.
`
workflowFile := filepath.Join(customDir, "test.md")
if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to create test workflow file: %v", err)
}

// Test 1: Compile with custom workflow directory should work
err = CompileWorkflows([]string{}, false, "", false, false, customDir, false, false, false)
if err != nil {
t.Errorf("CompileWorkflows with custom workflow-dir should succeed, got error: %v", err)
}

// Verify the lock file was created
lockFile := filepath.Join(customDir, "test.lock.yml")
if _, err := os.Stat(lockFile); os.IsNotExist(err) {
t.Error("Expected lock file to be created in custom directory")
}

// Test 2: Using absolute path should fail
err = CompileWorkflows([]string{}, false, "", false, false, "/absolute/path", false, false, false)
if err == nil {
t.Error("CompileWorkflows with absolute workflow-dir should fail")
}
if err != nil && err.Error() != "workflow-dir must be a relative path, got: /absolute/path" {
t.Errorf("Expected specific error message for absolute path, got: %v", err)
}

// Test 3: Empty workflow-dir should default to .github/workflows
// Create the default directory and a file
defaultDir := ".github/workflows"
if err := os.MkdirAll(defaultDir, 0755); err != nil {
t.Fatalf("Failed to create default workflow directory: %v", err)
}
defaultWorkflowFile := filepath.Join(defaultDir, "default.md")
if err := os.WriteFile(defaultWorkflowFile, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to create default workflow file: %v", err)
}

err = CompileWorkflows([]string{}, false, "", false, false, "", false, false, false)
if err != nil {
t.Errorf("CompileWorkflows with default workflow-dir should succeed, got error: %v", err)
}

// Verify the lock file was created in default location
defaultLockFile := filepath.Join(defaultDir, "default.lock.yml")
if _, err := os.Stat(defaultLockFile); os.IsNotExist(err) {
t.Error("Expected lock file to be created in default directory")
}
}

// TestCompileWorkflowsCustomDirValidation tests the validation of workflow directory paths
func TestCompileWorkflowsCustomDirValidation(t *testing.T) {
tests := []struct {
name string
workflowDir string
expectError bool
errorMsg string
}{
{
name: "empty string defaults to .github/workflows",
workflowDir: "",
expectError: false,
},
{
name: "relative path is valid",
workflowDir: "custom/workflows",
expectError: false,
},
{
name: "absolute path is invalid",
workflowDir: "/absolute/path",
expectError: true,
errorMsg: "workflow-dir must be a relative path, got: /absolute/path",
},
{
name: "path with .. is cleaned but valid",
workflowDir: "workflows/../workflows",
expectError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a temporary directory for each test
tmpDir, err := os.MkdirTemp("", "workflow-dir-validation-test")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tmpDir)

originalWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get current working directory: %v", err)
}
defer func() {
_ = os.Chdir(originalWd)
}()

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

// Initialize git repository properly
cmd := exec.Command("git", "init")
cmd.Dir = tmpDir
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to initialize git repository: %v", err)
}

// For non-error cases, create the expected directory
if !tt.expectError {
expectedDir := tt.workflowDir
if expectedDir == "" {
expectedDir = ".github/workflows"
}
if err := os.MkdirAll(expectedDir, 0755); err != nil {
t.Fatalf("Failed to create workflow directory: %v", err)
}
// Create a dummy workflow file
workflowFile := filepath.Join(expectedDir, "test.md")
workflowContent := `---
on: push
---

# Test Workflow
`
if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to create test workflow file: %v", err)
}
}

// Test the compilation
err = CompileWorkflows([]string{}, false, "", false, false, tt.workflowDir, false, false, false)

if tt.expectError {
if err == nil {
t.Errorf("Expected error for workflow-dir '%s', but got none", tt.workflowDir)
} else if err.Error() != tt.errorMsg {
t.Errorf("Expected error message '%s', got '%s'", tt.errorMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("Expected no error for workflow-dir '%s', but got: %v", tt.workflowDir, err)
}
}
})
}
}