diff --git a/Makefile b/Makefile index cb1b307b4f4..c00ceaac4e6 100644 --- a/Makefile +++ b/Makefile @@ -152,7 +152,7 @@ install: build .PHONY: recompile recompile: build ./$(BINARY_NAME) compile --validate --instructions - ./$(BINARY_NAME) compile --workflow-dir pkg/cli/workflows --validate; + ./$(BINARY_NAME) compile --workflows-dir pkg/cli/workflows --validate; # Run development server .PHONY: dev diff --git a/cmd/gh-aw/main.go b/cmd/gh-aw/main.go index f46a5323d66..960ab53f63e 100644 --- a/cmd/gh-aw/main.go +++ b/cmd/gh-aw/main.go @@ -88,18 +88,19 @@ It's a shortcut for: nameFlag, _ := cmd.Flags().GetString("name") prFlag, _ := cmd.Flags().GetBool("pr") forceFlag, _ := cmd.Flags().GetBool("force") + workflowsDir, _ := cmd.Flags().GetString("workflows-dir") if err := validateEngine(engineOverride); err != nil { fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error())) os.Exit(1) } if prFlag { - if err := cli.AddWorkflows(workflows, numberFlag, verbose, engineOverride, repoFlag, nameFlag, forceFlag, true); err != nil { + if err := cli.AddWorkflowsWithDir(workflows, numberFlag, verbose, engineOverride, repoFlag, nameFlag, forceFlag, true, workflowsDir); err != nil { fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error())) os.Exit(1) } } else { - if err := cli.AddWorkflows(workflows, numberFlag, verbose, engineOverride, repoFlag, nameFlag, forceFlag, false); err != nil { + if err := cli.AddWorkflowsWithDir(workflows, numberFlag, verbose, engineOverride, repoFlag, nameFlag, forceFlag, false, workflowsDir); err != nil { fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error())) os.Exit(1) } @@ -126,7 +127,8 @@ Examples: Run: func(cmd *cobra.Command, args []string) { workflowName := args[0] forceFlag, _ := cmd.Flags().GetBool("force") - if err := cli.NewWorkflow(workflowName, verbose, forceFlag); err != nil { + workflowsDir, _ := cmd.Flags().GetString("workflows-dir") + if err := cli.NewWorkflowWithDir(workflowName, verbose, forceFlag, workflowsDir); err != nil { fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error())) os.Exit(1) } @@ -142,7 +144,8 @@ var removeCmd = &cobra.Command{ pattern = args[0] } keepOrphans, _ := cmd.Flags().GetBool("keep-orphans") - if err := cli.RemoveWorkflows(pattern, keepOrphans); err != nil { + workflowsDir, _ := cmd.Flags().GetString("workflows-dir") + if err := cli.RemoveWorkflowsFromDir(pattern, keepOrphans, workflowsDir); err != nil { fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error())) os.Exit(1) } @@ -157,7 +160,8 @@ var statusCmd = &cobra.Command{ if len(args) > 0 { pattern = args[0] } - if err := cli.StatusWorkflows(pattern, verbose); err != nil { + workflowsDir, _ := cmd.Flags().GetString("workflows-dir") + if err := cli.StatusWorkflowsInDir(pattern, verbose, workflowsDir); err != nil { fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error())) os.Exit(1) } @@ -172,7 +176,8 @@ var enableCmd = &cobra.Command{ if len(args) > 0 { pattern = args[0] } - if err := cli.EnableWorkflows(pattern); err != nil { + workflowsDir, _ := cmd.Flags().GetString("workflows-dir") + if err := cli.EnableWorkflowsInDir(pattern, workflowsDir); err != nil { fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error())) os.Exit(1) } @@ -187,7 +192,8 @@ var disableCmd = &cobra.Command{ if len(args) > 0 { pattern = args[0] } - if err := cli.DisableWorkflows(pattern); err != nil { + workflowsDir, _ := cmd.Flags().GetString("workflows-dir") + if err := cli.DisableWorkflowsInDir(pattern, workflowsDir); err != nil { fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error())) os.Exit(1) } @@ -206,13 +212,13 @@ 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 --workflows-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") + workflowDir, _ := cmd.Flags().GetString("workflows-dir") instructions, _ := cmd.Flags().GetBool("instructions") noEmit, _ := cmd.Flags().GetBool("no-emit") purge, _ := cmd.Flags().GetBool("purge") @@ -332,9 +338,15 @@ func init() { // Add force flag to add command addCmd.Flags().Bool("force", false, "Overwrite existing workflow files") + // Add workflows-dir flag to add command + addCmd.Flags().String("workflows-dir", "", "Relative directory containing workflows (default: .github/workflows)") + // Add force flag to new command newCmd.Flags().Bool("force", false, "Overwrite existing workflow files") + // Add workflows-dir flag to new command + newCmd.Flags().String("workflows-dir", "", "Relative directory containing workflows (default: .github/workflows)") + // Add packages flag to list command listCmd.Flags().BoolP("packages", "p", false, "List installed packages instead of available workflows") listCmd.Flags().BoolP("local", "l", false, "List local packages instead of global packages (requires --packages)") @@ -349,13 +361,21 @@ 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().String("workflows-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)") // Add flags to remove command removeCmd.Flags().Bool("keep-orphans", false, "Skip removal of orphaned include files that are no longer referenced by any workflow") + removeCmd.Flags().String("workflows-dir", "", "Relative directory containing workflows (default: .github/workflows)") + + // Add workflows-dir flag to status command + statusCmd.Flags().String("workflows-dir", "", "Relative directory containing workflows (default: .github/workflows)") + + // Add workflows-dir flag to enable and disable commands + enableCmd.Flags().String("workflows-dir", "", "Relative directory containing workflows (default: .github/workflows)") + disableCmd.Flags().String("workflows-dir", "", "Relative directory containing workflows (default: .github/workflows)") // Add flags to run command runCmd.Flags().Int("repeat", 0, "Repeat running workflows every SECONDS (0 = run once)") diff --git a/pkg/cli/commands.go b/pkg/cli/commands.go index 4a4a9ba2337..4662653c8c2 100644 --- a/pkg/cli/commands.go +++ b/pkg/cli/commands.go @@ -158,6 +158,10 @@ func listAgenticEngines(verbose bool) error { // AddWorkflows adds one or more workflows from components to .github/workflows // with optional repository installation and PR creation func AddWorkflows(workflows []string, number int, verbose bool, engineOverride string, repoSpec string, name string, force bool, createPR bool) error { + return AddWorkflowsWithDir(workflows, number, verbose, engineOverride, repoSpec, name, force, createPR, "") +} + +func AddWorkflowsWithDir(workflows []string, number int, verbose bool, engineOverride string, repoSpec string, name string, force bool, createPR bool, workflowsDir string) error { if len(workflows) == 0 { return fmt.Errorf("at least one workflow name is required") } @@ -209,15 +213,15 @@ func AddWorkflows(workflows []string, number int, verbose bool, engineOverride s // Handle PR creation workflow if createPR { - return addWorkflowsWithPR(workflows, number, verbose, engineOverride, name, force) + return addWorkflowsWithPR(workflows, number, verbose, engineOverride, name, force, workflowsDir) } // Handle normal workflow addition - return addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force) + return addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, workflowsDir) } // addWorkflowsNormal handles normal workflow addition without PR creation -func addWorkflowsNormal(workflows []string, number int, verbose bool, engineOverride string, name string, force bool) error { +func addWorkflowsNormal(workflows []string, number int, verbose bool, engineOverride string, name string, force bool, workflowsDir string) error { // Create file tracker for all operations tracker, err := NewFileTracker() if err != nil { @@ -244,7 +248,7 @@ func addWorkflowsNormal(workflows []string, number int, verbose bool, engineOver currentName = name } - if err := AddWorkflowWithTracking(workflow, number, verbose, engineOverride, currentName, force, tracker); err != nil { + if err := AddWorkflowWithTracking(workflow, number, verbose, engineOverride, currentName, force, tracker, workflowsDir); err != nil { return fmt.Errorf("failed to add workflow '%s': %w", workflow, err) } } @@ -257,7 +261,7 @@ func addWorkflowsNormal(workflows []string, number int, verbose bool, engineOver } // addWorkflowsWithPR handles workflow addition with PR creation -func addWorkflowsWithPR(workflows []string, number int, verbose bool, engineOverride string, name string, force bool) error { +func addWorkflowsWithPR(workflows []string, number int, verbose bool, engineOverride string, name string, force bool, workflowsDir string) error { // Get current branch for restoration later currentBranch, err := getCurrentBranch() if err != nil { @@ -295,7 +299,7 @@ func addWorkflowsWithPR(workflows []string, number int, verbose bool, engineOver }() // Add workflows using the normal function logic - if err := addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force); err != nil { + if err := addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, workflowsDir); err != nil { // Rollback on error if rollbackErr := tracker.RollbackAllFiles(verbose); rollbackErr != nil && verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to rollback files: %v", rollbackErr))) @@ -410,7 +414,7 @@ func AddMultipleWorkflowsWithRepoAndPR(workflows []string, number int, verbose b } // AddWorkflowWithTracking adds a workflow from components to .github/workflows with file tracking -func AddWorkflowWithTracking(workflow string, number int, verbose bool, engineOverride string, name string, force bool, tracker *FileTracker) error { +func AddWorkflowWithTracking(workflow string, number int, verbose bool, engineOverride string, name string, force bool, tracker *FileTracker, workflowsDir string) error { if workflow == "" { fmt.Fprintln(os.Stderr, console.FormatErrorMessage("No components path specified. Usage: "+constants.CLIExtensionPrefix+" add ")) // Show available workflows using the same logic as ListWorkflows @@ -434,7 +438,7 @@ func AddWorkflowWithTracking(workflow string, number int, verbose bool, engineOv fmt.Println("Locating workflow components...") } - workflowsDir := getWorkflowsDir() + workflowsDir = getWorkflowsDir(workflowsDir) // Add .md extension if not present workflowPath := workflow @@ -467,10 +471,11 @@ func AddWorkflowWithTracking(workflow string, number int, verbose bool, engineOv return fmt.Errorf("add workflow requires being in a git repository: %w", err) } - // Ensure .github/workflows directory exists relative to git root - githubWorkflowsDir := filepath.Join(gitRoot, ".github/workflows") + // Ensure workflows directory exists relative to git root + targetWorkflowsDir := getWorkflowsDir(workflowsDir) + githubWorkflowsDir := filepath.Join(gitRoot, targetWorkflowsDir) if err := os.MkdirAll(githubWorkflowsDir, 0755); err != nil { - return fmt.Errorf("failed to create .github/workflows directory: %w", err) + return fmt.Errorf("failed to create %s directory: %w", targetWorkflowsDir, err) } // Determine the filename to use @@ -487,7 +492,7 @@ func AddWorkflowWithTracking(workflow string, number int, verbose bool, engineOv // Check if a workflow with this name already exists existingFile := filepath.Join(githubWorkflowsDir, filename+".md") if _, err := os.Stat(existingFile); err == nil && !force { - return fmt.Errorf("workflow '%s' already exists in .github/workflows/. Use a different name with -n flag, remove the existing workflow first, or use --force to overwrite", filename) + return fmt.Errorf("workflow '%s' already exists in %s/. Use a different name with -n flag, remove the existing workflow first, or use --force to overwrite", filename, targetWorkflowsDir) } // Collect all @include dependencies from the workflow file @@ -496,14 +501,14 @@ func AddWorkflowWithTracking(workflow string, number int, verbose bool, engineOv fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Failed to collect include dependencies: %v", err))) } - // Copy all @include dependencies to .github/workflows maintaining relative paths + // Copy all @include dependencies to workflows directory maintaining relative paths if err := copyIncludeDependenciesFromSourceWithForce(includeDeps, githubWorkflowsDir, sourceInfo, verbose, force, tracker); err != nil { fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Failed to copy include dependencies: %v", err))) } // Process each copy for i := 1; i <= number; i++ { - // Construct the destination file path with numbering in .github/workflows + // Construct the destination file path with numbering in workflows directory var destFile string if number == 1 { destFile = filepath.Join(githubWorkflowsDir, filename+".md") @@ -615,7 +620,7 @@ func CompileWorkflows(markdownFiles []string, verbose bool, engineOverride strin } else { // Ensure the path is relative if filepath.IsAbs(workflowDir) { - return fmt.Errorf("workflow-dir must be a relative path, got: %s", workflowDir) + return fmt.Errorf("workflows-dir must be a relative path, got: %s", workflowDir) } // Clean the path to avoid issues with ".." or other problematic elements workflowDir = filepath.Clean(workflowDir) @@ -1076,14 +1081,18 @@ func handleFileDeleted(mdFile string, verbose bool) { // RemoveWorkflows removes workflows matching a pattern func RemoveWorkflows(pattern string, keepOrphans bool) error { - workflowsDir := getWorkflowsDir() + return RemoveWorkflowsFromDir(pattern, keepOrphans, "") +} + +func RemoveWorkflowsFromDir(pattern string, keepOrphans bool, workflowsDir string) error { + workflowsDir = getWorkflowsDir(workflowsDir) if _, err := os.Stat(workflowsDir); os.IsNotExist(err) { - fmt.Println("No .github/workflows directory found.") + fmt.Printf("No %s directory found.\n", workflowsDir) return nil } - // Find all markdown files in .github/workflows + // Find all markdown files in workflows directory mdFiles, err := filepath.Glob(filepath.Join(workflowsDir, "*.md")) if err != nil { return fmt.Errorf("failed to find workflow files: %w", err) @@ -1135,7 +1144,7 @@ func RemoveWorkflows(pattern string, keepOrphans bool) error { var orphanedIncludes []string if !keepOrphans { var err error - orphanedIncludes, err = previewOrphanedIncludes(filesToRemove, false) + orphanedIncludes, err = previewOrphanedIncludes(filesToRemove, false, workflowsDir) if err != nil { fmt.Printf("Warning: Failed to preview orphaned includes: %v\n", err) orphanedIncludes = []string{} // Continue with empty list @@ -1201,7 +1210,7 @@ func RemoveWorkflows(pattern string, keepOrphans bool) error { // Clean up orphaned include files (if orphan removal is enabled) if len(removedFiles) > 0 && !keepOrphans { - if err := cleanupOrphanedIncludes(false); err != nil { + if err := cleanupOrphanedIncludes(false, workflowsDir); err != nil { fmt.Printf("Warning: Failed to clean up orphaned includes: %v\n", err) } } @@ -1216,15 +1225,15 @@ func RemoveWorkflows(pattern string, keepOrphans bool) error { // StatusWorkflows shows status of workflows // getMarkdownWorkflowFiles finds all markdown files in .github/workflows directory -func getMarkdownWorkflowFiles() ([]string, error) { - workflowsDir := getWorkflowsDir() +func getMarkdownWorkflowFiles(workflowsDir ...string) ([]string, error) { + targetWorkflowsDir := getWorkflowsDir(workflowsDir...) - if _, err := os.Stat(workflowsDir); os.IsNotExist(err) { - return nil, fmt.Errorf("no .github/workflows directory found") + if _, err := os.Stat(targetWorkflowsDir); os.IsNotExist(err) { + return nil, fmt.Errorf("no %s directory found", targetWorkflowsDir) } - // Find all markdown files in .github/workflows - mdFiles, err := filepath.Glob(filepath.Join(workflowsDir, "*.md")) + // Find all markdown files in workflows directory + mdFiles, err := filepath.Glob(filepath.Join(targetWorkflowsDir, "*.md")) if err != nil { return nil, fmt.Errorf("failed to find workflow files: %w", err) } @@ -1233,6 +1242,10 @@ func getMarkdownWorkflowFiles() ([]string, error) { } func StatusWorkflows(pattern string, verbose bool) error { + return StatusWorkflowsInDir(pattern, verbose, "") +} + +func StatusWorkflowsInDir(pattern string, verbose bool, workflowsDir string) error { if verbose { fmt.Printf("Checking status of workflow files\n") if pattern != "" { @@ -1240,7 +1253,7 @@ func StatusWorkflows(pattern string, verbose bool) error { } } - mdFiles, err := getMarkdownWorkflowFiles() + mdFiles, err := getMarkdownWorkflowFiles(workflowsDir) if err != nil { fmt.Println(err.Error()) return nil @@ -1395,16 +1408,24 @@ func calculateTimeRemaining(stopTimeStr string) string { // EnableWorkflows enables workflows matching a pattern func EnableWorkflows(pattern string) error { - return toggleWorkflows(pattern, true) + return EnableWorkflowsInDir(pattern, "") +} + +func EnableWorkflowsInDir(pattern string, workflowsDir string) error { + return toggleWorkflows(pattern, true, workflowsDir) } // DisableWorkflows disables workflows matching a pattern func DisableWorkflows(pattern string) error { - return toggleWorkflows(pattern, false) + return DisableWorkflowsInDir(pattern, "") +} + +func DisableWorkflowsInDir(pattern string, workflowsDir string) error { + return toggleWorkflows(pattern, false, workflowsDir) } // Helper function to toggle workflows -func toggleWorkflows(pattern string, enable bool) error { +func toggleWorkflows(pattern string, enable bool, workflowsDir string) error { action := "enable" if !enable { action = "disable" @@ -1415,8 +1436,8 @@ func toggleWorkflows(pattern string, enable bool) error { return fmt.Errorf("GitHub CLI (gh) is required but not available") } - // Get the core set of workflows from markdown files in .github/workflows - mdFiles, err := getMarkdownWorkflowFiles() + // Get the core set of workflows from markdown files in workflows directory + mdFiles, err := getMarkdownWorkflowFiles(workflowsDir) if err != nil { // Handle missing .github/workflows directory gracefully fmt.Printf("No workflow files found to %s.\n", action) @@ -2437,7 +2458,7 @@ func findAndReadWorkflow(workflowPath, workflowsDir string, verbose bool) ([]byt // If not found in local, try packages if verbose { - fmt.Printf("Workflow not found in local .github/workflows or local components, searching packages...\n") + fmt.Printf("Workflow not found in local %s or local components, searching packages...\n", workflowsDir) } return findWorkflowInPackages(workflowPath, verbose) @@ -2803,15 +2824,15 @@ func copyIncludeDependenciesFromPackageWithForce(dependencies []IncludeDependenc } // cleanupOrphanedIncludes removes include files that are no longer used by any workflow -func cleanupOrphanedIncludes(verbose bool) error { +func cleanupOrphanedIncludes(verbose bool, workflowsDir ...string) error { // Get all remaining markdown files - mdFiles, err := getMarkdownWorkflowFiles() + mdFiles, err := getMarkdownWorkflowFiles(workflowsDir...) if err != nil { // No markdown files means we can clean up all includes if verbose { fmt.Printf("No markdown files found, cleaning up all includes\n") } - return cleanupAllIncludes(verbose) + return cleanupAllIncludes(verbose, workflowsDir...) } // Collect all include dependencies from remaining workflows @@ -2840,19 +2861,19 @@ func cleanupOrphanedIncludes(verbose bool) error { } } - // Find all include files in .github/workflows + // Find all include files in workflows directory // Only consider files in subdirectories (like shared/) as potential include files // Root-level .md files are workflow files, not include files - workflowsDir := ".github/workflows" + targetWorkflowsDir := getWorkflowsDir(workflowsDir...) var allIncludes []string - err = filepath.Walk(workflowsDir, func(path string, info os.FileInfo, err error) error { + err = filepath.Walk(targetWorkflowsDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() && strings.HasSuffix(info.Name(), ".md") { - relPath, err := filepath.Rel(workflowsDir, path) + relPath, err := filepath.Rel(targetWorkflowsDir, path) if err != nil { return err } @@ -2874,7 +2895,7 @@ func cleanupOrphanedIncludes(verbose bool) error { // Remove unused includes for _, include := range allIncludes { if !usedIncludes[include] { - includePath := filepath.Join(workflowsDir, include) + includePath := filepath.Join(targetWorkflowsDir, include) if err := os.Remove(includePath); err != nil { if verbose { fmt.Printf("Warning: Failed to remove orphaned include %s: %v\n", include, err) @@ -2889,9 +2910,9 @@ func cleanupOrphanedIncludes(verbose bool) error { } // previewOrphanedIncludes returns a list of include files that would become orphaned if the specified files were removed -func previewOrphanedIncludes(filesToRemove []string, verbose bool) ([]string, error) { +func previewOrphanedIncludes(filesToRemove []string, verbose bool, workflowsDir ...string) ([]string, error) { // Get all current markdown files - allMdFiles, err := getMarkdownWorkflowFiles() + allMdFiles, err := getMarkdownWorkflowFiles(workflowsDir...) if err != nil { return nil, err } @@ -2912,7 +2933,7 @@ func previewOrphanedIncludes(filesToRemove []string, verbose bool) ([]string, er // If no files remain, all include files would be orphaned if len(remainingFiles) == 0 { - return getAllIncludeFiles() + return getAllIncludeFiles(workflowsDir...) } // Collect all include dependencies from remaining workflows @@ -2942,7 +2963,7 @@ func previewOrphanedIncludes(filesToRemove []string, verbose bool) ([]string, er } // Find all include files and check which ones would be orphaned - allIncludes, err := getAllIncludeFiles() + allIncludes, err := getAllIncludeFiles(workflowsDir...) if err != nil { return nil, err } @@ -2957,18 +2978,18 @@ func previewOrphanedIncludes(filesToRemove []string, verbose bool) ([]string, er return orphanedIncludes, nil } -// getAllIncludeFiles returns all include files in .github/workflows subdirectories -func getAllIncludeFiles() ([]string, error) { - workflowsDir := ".github/workflows" +// getAllIncludeFiles returns all include files in workflows subdirectories +func getAllIncludeFiles(workflowsDir ...string) ([]string, error) { + targetWorkflowsDir := getWorkflowsDir(workflowsDir...) var allIncludes []string - err := filepath.Walk(workflowsDir, func(path string, info os.FileInfo, err error) error { + err := filepath.Walk(targetWorkflowsDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() && strings.HasSuffix(info.Name(), ".md") { - relPath, err := filepath.Rel(workflowsDir, path) + relPath, err := filepath.Rel(targetWorkflowsDir, path) if err != nil { return err } @@ -2987,16 +3008,16 @@ func getAllIncludeFiles() ([]string, error) { } // cleanupAllIncludes removes all include files when no workflows remain -func cleanupAllIncludes(verbose bool) error { - workflowsDir := ".github/workflows" +func cleanupAllIncludes(verbose bool, workflowsDir ...string) error { + targetWorkflowsDir := getWorkflowsDir(workflowsDir...) - err := filepath.Walk(workflowsDir, func(path string, info os.FileInfo, err error) error { + err := filepath.Walk(targetWorkflowsDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() && strings.HasSuffix(info.Name(), ".md") { - relPath, _ := filepath.Rel(workflowsDir, path) + relPath, _ := filepath.Rel(targetWorkflowsDir, path) // Only remove files in subdirectories (like shared/) as these are include files // Root-level .md files are workflow files, not include files @@ -3188,7 +3209,7 @@ func resolveWorkflowFile(fileOrWorkflowName string, verbose bool) (string, error // Try to find the workflow from multiple sources sourceContent, sourceInfo, err := findAndReadWorkflow(workflowPath, workflowsDir, verbose) if err != nil { - return "", fmt.Errorf("workflow '%s' not found in local .github/workflows, components or packages", fileOrWorkflowName) + return "", fmt.Errorf("workflow '%s' not found in local %s, components or packages", fileOrWorkflowName, workflowsDir) } // If we found the workflow in packages, @@ -3785,20 +3806,27 @@ func createPR(branchName, title, body string, verbose bool) error { // NewWorkflow creates a new workflow markdown file with template content func NewWorkflow(workflowName string, verbose bool, force bool) error { + return NewWorkflowWithDir(workflowName, verbose, force, "") +} + +func NewWorkflowWithDir(workflowName string, verbose bool, force bool, workflowsDir string) error { if verbose { fmt.Printf("Creating new workflow: %s\n", workflowName) } - // Get current working directory for .github/workflows + // Get current working directory for workflows directory workingDir, err := os.Getwd() if err != nil { return fmt.Errorf("failed to get current working directory: %w", err) } - // Create .github/workflows directory if it doesn't exist - githubWorkflowsDir := filepath.Join(workingDir, ".github", "workflows") + // Determine the workflows directory to use + targetWorkflowsDir := getWorkflowsDir(workflowsDir) + + // Create workflows directory if it doesn't exist + githubWorkflowsDir := filepath.Join(workingDir, targetWorkflowsDir) if err := os.MkdirAll(githubWorkflowsDir, 0755); err != nil { - return fmt.Errorf("failed to create .github/workflows directory: %w", err) + return fmt.Errorf("failed to create %s directory: %w", targetWorkflowsDir, err) } // Construct the destination file path diff --git a/pkg/cli/commands_test.go b/pkg/cli/commands_test.go index 018a35256e7..72af2e095aa 100644 --- a/pkg/cli/commands_test.go +++ b/pkg/cli/commands_test.go @@ -57,7 +57,7 @@ func TestAddWorkflow(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := AddWorkflowWithTracking(tt.workflow, tt.number, false, "", "", false, nil) + err := AddWorkflowWithTracking(tt.workflow, tt.number, false, "", "", false, nil, "") if tt.expectError && err == nil { t.Errorf("Expected error for test '%s', got nil", tt.name) @@ -74,13 +74,13 @@ func TestAddWorkflowForce(t *testing.T) { // It doesn't test the actual file system operations // Test that force=false fails when a file "exists" (simulated by empty workflow name which triggers help) - err := AddWorkflowWithTracking("", 1, false, "", "", false, nil) + err := AddWorkflowWithTracking("", 1, false, "", "", false, nil, "") if err != nil { t.Errorf("Expected no error for empty workflow (shows help), got: %v", err) } // Test that force=true works with same parameters - err = AddWorkflowWithTracking("", 1, false, "", "", true, nil) + err = AddWorkflowWithTracking("", 1, false, "", "", true, nil, "") if err != nil { t.Errorf("Expected no error for empty workflow with force=true, got: %v", err) } @@ -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 { diff --git a/pkg/cli/workflows.go b/pkg/cli/workflows.go index a7d02947e2f..8744c0ee59b 100644 --- a/pkg/cli/workflows.go +++ b/pkg/cli/workflows.go @@ -22,7 +22,10 @@ func getPackagesDir(local bool) (string, error) { return filepath.Join(homeDir, ".aw", "packages"), nil } -func getWorkflowsDir() string { +func getWorkflowsDir(customDir ...string) string { + if len(customDir) > 0 && customDir[0] != "" { + return customDir[0] + } return ".github/workflows" } diff --git a/pkg/cli/workflow_dir_test.go b/pkg/cli/workflows_dir_test.go similarity index 87% rename from pkg/cli/workflow_dir_test.go rename to pkg/cli/workflows_dir_test.go index 2ab33d001b1..c5faa70f073 100644 --- a/pkg/cli/workflow_dir_test.go +++ b/pkg/cli/workflows_dir_test.go @@ -7,7 +7,7 @@ import ( "testing" ) -// TestCompileWorkflowsWithCustomWorkflowDir tests the --workflow-dir flag functionality +// TestCompileWorkflowsWithCustomWorkflowDir tests the --workflows-dir flag functionality func TestCompileWorkflowsWithCustomWorkflowDir(t *testing.T) { // Save current directory and defer restoration originalWd, err := os.Getwd() @@ -19,7 +19,7 @@ func TestCompileWorkflowsWithCustomWorkflowDir(t *testing.T) { }() // Create a temporary git repository with custom workflow directory - tmpDir, err := os.MkdirTemp("", "workflow-dir-test") + tmpDir, err := os.MkdirTemp("", "workflows-dir-test") if err != nil { t.Fatalf("Failed to create temp directory: %v", err) } @@ -72,13 +72,13 @@ This is a test workflow in a 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") + t.Error("CompileWorkflows with absolute workflows-dir should fail") } - if err != nil && err.Error() != "workflow-dir must be a relative path, got: /absolute/path" { + if err != nil && err.Error() != "workflows-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 + // Test 3: Empty workflows-dir should default to .github/workflows // Create the default directory and a file defaultDir := ".github/workflows" if err := os.MkdirAll(defaultDir, 0755); err != nil { @@ -91,7 +91,7 @@ This is a test workflow in a custom directory. 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) + t.Errorf("CompileWorkflows with default workflows-dir should succeed, got error: %v", err) } // Verify the lock file was created in default location @@ -101,7 +101,7 @@ This is a test workflow in a custom directory. } } -// TestCompileWorkflowsCustomDirValidation tests the validation of workflow directory paths +// TestCompileWorkflowsCustomDirValidation tests the validation of workflows directory paths func TestCompileWorkflowsCustomDirValidation(t *testing.T) { tests := []struct { name string @@ -123,7 +123,7 @@ func TestCompileWorkflowsCustomDirValidation(t *testing.T) { name: "absolute path is invalid", workflowDir: "/absolute/path", expectError: true, - errorMsg: "workflow-dir must be a relative path, got: /absolute/path", + errorMsg: "workflows-dir must be a relative path, got: /absolute/path", }, { name: "path with .. is cleaned but valid", @@ -135,7 +135,7 @@ func TestCompileWorkflowsCustomDirValidation(t *testing.T) { 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") + tmpDir, err := os.MkdirTemp("", "workflows-dir-validation-test") if err != nil { t.Fatalf("Failed to create temp directory: %v", err) } @@ -187,13 +187,13 @@ on: push if tt.expectError { if err == nil { - t.Errorf("Expected error for workflow-dir '%s', but got none", tt.workflowDir) + t.Errorf("Expected error for workflows-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) + t.Errorf("Expected no error for workflows-dir '%s', but got: %v", tt.workflowDir, err) } } })