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
6 changes: 5 additions & 1 deletion cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ var removeCmd = &cobra.Command{
if len(args) > 0 {
pattern = args[0]
}
if err := cli.RemoveWorkflows(pattern); err != nil {
keepOrphans, _ := cmd.Flags().GetBool("keep-orphans")
if err := cli.RemoveWorkflows(pattern, keepOrphans); err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
os.Exit(1)
}
Expand Down Expand Up @@ -322,6 +323,9 @@ func init() {
compileCmd.Flags().Bool("auto-compile", false, "Generate auto-compile workflow file for automatic compilation")
compileCmd.Flags().BoolP("watch", "w", false, "Watch for changes to workflow files and recompile automatically")

// 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")

// Add all commands to root
rootCmd.AddCommand(listCmd)
rootCmd.AddCommand(addCmd)
Expand Down
3 changes: 3 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ gh aw compile --watch --auto-compile --verbose
# Remove a workflow
gh aw remove WorkflowName

# Remove a workflow without removing orphaned include files
gh aw remove WorkflowName --keep-orphans

# Run a workflow immediately
gh aw run WorkflowName

Expand Down
150 changes: 139 additions & 11 deletions pkg/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ func handleFileDeleted(mdFile string, verbose bool) {
}

// RemoveWorkflows removes workflows matching a pattern
func RemoveWorkflows(pattern string) error {
func RemoveWorkflows(pattern string, keepOrphans bool) error {
workflowsDir := getWorkflowsDir()

if _, err := os.Stat(workflowsDir); os.IsNotExist(err) {
Expand Down Expand Up @@ -905,6 +905,17 @@ func RemoveWorkflows(pattern string) error {
return nil
}

// Preview orphaned includes that would be removed (if orphan removal is enabled)
var orphanedIncludes []string
if !keepOrphans {
var err error
orphanedIncludes, err = previewOrphanedIncludes(filesToRemove, false)
if err != nil {
fmt.Printf("Warning: Failed to preview orphaned includes: %v\n", err)
orphanedIncludes = []string{} // Continue with empty list
}
}

// Show what will be removed
fmt.Printf("The following workflows will be removed:\n")
for _, file := range filesToRemove {
Expand All @@ -922,6 +933,14 @@ func RemoveWorkflows(pattern string) error {
}
}

// Show orphaned includes that will also be removed
if len(orphanedIncludes) > 0 {
fmt.Printf("\nThe following orphaned include files will also be removed (suppress with --keep-orphans):\n")
for _, include := range orphanedIncludes {
fmt.Printf(" %s (orphaned include)\n", include)
}
}

// Ask for confirmation
fmt.Print("\nAre you sure you want to remove these workflows? [y/N]: ")
reader := bufio.NewReader(os.Stdin)
Expand Down Expand Up @@ -954,8 +973,8 @@ func RemoveWorkflows(pattern string) error {
}
}

// Clean up orphaned include files
if len(removedFiles) > 0 {
// Clean up orphaned include files (if orphan removal is enabled)
if len(removedFiles) > 0 && !keepOrphans {
if err := cleanupOrphanedIncludes(false); err != nil {
fmt.Printf("Warning: Failed to clean up orphaned includes: %v\n", err)
}
Expand Down Expand Up @@ -2438,6 +2457,8 @@ func cleanupOrphanedIncludes(verbose bool) error {
}

// Find all include files in .github/workflows
// Only consider files in subdirectories (like shared/) as potential include files
// Root-level .md files are workflow files, not include files
workflowsDir := ".github/workflows"
var allIncludes []string

Expand All @@ -2447,12 +2468,16 @@ func cleanupOrphanedIncludes(verbose bool) error {
}

if !info.IsDir() && strings.HasSuffix(info.Name(), ".md") {
// This is an include file
relPath, err := filepath.Rel(workflowsDir, path)
if err != nil {
return err
}
allIncludes = append(allIncludes, relPath)

// Only consider files in subdirectories as potential include files
// Root-level .md files are workflow files, not include files
if strings.Contains(relPath, string(filepath.Separator)) {
allIncludes = append(allIncludes, relPath)
}
}

return nil
Expand All @@ -2479,6 +2504,104 @@ func cleanupOrphanedIncludes(verbose bool) error {
return nil
}

// 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) {
// Get all current markdown files
allMdFiles, err := getMarkdownWorkflowFiles()
if err != nil {
return nil, err
}

// Create a map of files to remove for quick lookup
removeMap := make(map[string]bool)
for _, file := range filesToRemove {
removeMap[file] = true
}

// Get the files that would remain after removal
var remainingFiles []string
for _, file := range allMdFiles {
if !removeMap[file] {
remainingFiles = append(remainingFiles, file)
}
}

// If no files remain, all include files would be orphaned
if len(remainingFiles) == 0 {
return getAllIncludeFiles()
}

// Collect all include dependencies from remaining workflows
usedIncludes := make(map[string]bool)

for _, mdFile := range remainingFiles {
content, err := os.ReadFile(mdFile)
if err != nil {
if verbose {
fmt.Printf("Warning: Could not read %s for include analysis: %v\n", mdFile, err)
}
continue
}

// Find includes used by this workflow
includes, err := findIncludesInContent(string(content), filepath.Dir(mdFile), verbose)
if err != nil {
if verbose {
fmt.Printf("Warning: Could not analyze includes in %s: %v\n", mdFile, err)
}
continue
}

for _, include := range includes {
usedIncludes[include] = true
}
}

// Find all include files and check which ones would be orphaned
allIncludes, err := getAllIncludeFiles()
if err != nil {
return nil, err
}

var orphanedIncludes []string
for _, include := range allIncludes {
if !usedIncludes[include] {
orphanedIncludes = append(orphanedIncludes, include)
}
}

return orphanedIncludes, nil
}

// getAllIncludeFiles returns all include files in .github/workflows subdirectories
func getAllIncludeFiles() ([]string, error) {
workflowsDir := ".github/workflows"
var allIncludes []string

err := filepath.Walk(workflowsDir, 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)
if err != nil {
return err
}

// Only consider files in subdirectories as potential include files
// Root-level .md files are workflow files, not include files
if strings.Contains(relPath, string(filepath.Separator)) {
allIncludes = append(allIncludes, relPath)
}
}

return nil
})

return allIncludes, err
}

// cleanupAllIncludes removes all include files when no workflows remain
func cleanupAllIncludes(verbose bool) error {
workflowsDir := ".github/workflows"
Expand All @@ -2489,13 +2612,18 @@ func cleanupAllIncludes(verbose bool) error {
}

if !info.IsDir() && strings.HasSuffix(info.Name(), ".md") {
if err := os.Remove(path); err != nil {
if verbose {
fmt.Printf("Warning: Failed to remove include %s: %v\n", path, err)
relPath, _ := filepath.Rel(workflowsDir, path)

// Only remove files in subdirectories (like shared/) as these are include files
// Root-level .md files are workflow files, not include files
if strings.Contains(relPath, string(filepath.Separator)) {
if err := os.Remove(path); err != nil {
if verbose {
fmt.Printf("Warning: Failed to remove include %s: %v\n", relPath, err)
}
} else {
fmt.Printf("Removed include: %s\n", relPath)
}
} else {
relPath, _ := filepath.Rel(workflowsDir, path)
fmt.Printf("Removed include: %s\n", relPath)
}
}

Expand Down
Loading