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
83 changes: 74 additions & 9 deletions pkg/cli/add_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,68 @@ func handleRepoOnlySpec(repoSpec string, verbose bool) error {
return nil
}

// displayAvailableWorkflows lists available workflows from an installed package
func displayAvailableWorkflows(repoSlug, version string, verbose bool) error {
addLog.Printf("Displaying available workflows for repository: %s", repoSlug)

// List workflows in the installed package
workflows, err := listWorkflowsInPackage(repoSlug, verbose)
if err != nil {
return fmt.Errorf("failed to list workflows in %s: %w", repoSlug, err)
}

// Display the list of available workflows
if len(workflows) == 0 {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("No workflows found in repository %s", repoSlug)))
return nil
}

fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Available workflows in %s:", repoSlug)))
fmt.Fprintln(os.Stderr, "")

for _, workflow := range workflows {
// Extract workflow name from path (remove .md extension and path)
workflowName := strings.TrimSuffix(filepath.Base(workflow), ".md")

// For workflows in workflows/ directory, show simplified name
if strings.HasPrefix(workflow, "workflows/") {
workflowName = strings.TrimSuffix(strings.TrimPrefix(workflow, "workflows/"), ".md")
}
Comment on lines +320 to +326
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated workflow name extraction logic: The logic for extracting workflow names from paths (lines 320-326) is duplicated almost identically in lines 344-348 for the example workflow. Consider extracting this into a small helper function to avoid code duplication and make the logic easier to maintain.

Copilot uses AI. Check for mistakes.

// Build the full command
fullSpec := fmt.Sprintf("%s/%s", repoSlug, workflowName)
if version != "" {
fullSpec += "@" + version
}

fmt.Fprintf(os.Stderr, " • %s\n", workflowName)
if verbose {
fmt.Fprintf(os.Stderr, " Command: %s add %s\n", constants.CLIExtensionPrefix, fullSpec)
}
}

fmt.Fprintln(os.Stderr, "")
fmt.Fprintf(os.Stderr, "Example:\n")

// Show example with first workflow
exampleWorkflow := workflows[0]
exampleName := strings.TrimSuffix(filepath.Base(exampleWorkflow), ".md")
if strings.HasPrefix(exampleWorkflow, "workflows/") {
exampleName = strings.TrimSuffix(strings.TrimPrefix(exampleWorkflow, "workflows/"), ".md")
}

exampleSpec := fmt.Sprintf("%s/%s", repoSlug, exampleName)
if version != "" {
exampleSpec += "@" + version
}

fmt.Fprintf(os.Stderr, " %s add %s\n", constants.CLIExtensionPrefix, exampleSpec)
fmt.Fprintln(os.Stderr, "")

return nil
}

// addWorkflowsNormal handles normal workflow addition without PR creation
func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, noGitattributes bool) error {
// Create file tracker for all operations
Expand Down Expand Up @@ -488,15 +550,18 @@ func addWorkflowWithTracking(workflow *WorkflowSpec, number int, verbose bool, e
if err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Workflow '%s' not found.", workflowPath)))

// Provide information about workflow repositories
fmt.Println("\nTo add workflows to your project:")
fmt.Println("=================================")
fmt.Println("Use the 'add' command with repository/workflow specifications:")
fmt.Println(" " + constants.CLIExtensionPrefix + " add owner/repo/workflow-name")
fmt.Println(" " + constants.CLIExtensionPrefix + " add owner/repo/workflow-name@version")
fmt.Println("\nExample:")
fmt.Println(" " + constants.CLIExtensionPrefix + " add githubnext/agentics/ci-doctor")
fmt.Println(" " + constants.CLIExtensionPrefix + " add githubnext/agentics/daily-plan@main")
// Try to list available workflows from the installed package
if err := displayAvailableWorkflows(workflow.RepoSlug, workflow.Version, verbose); err != nil {
// If we can't list workflows, provide generic help
fmt.Println("\nTo add workflows to your project:")
fmt.Println("=================================")
fmt.Println("Use the 'add' command with repository/workflow specifications:")
fmt.Println(" " + constants.CLIExtensionPrefix + " add owner/repo/workflow-name")
fmt.Println(" " + constants.CLIExtensionPrefix + " add owner/repo/workflow-name@version")
fmt.Println("\nExample:")
fmt.Println(" " + constants.CLIExtensionPrefix + " add githubnext/agentics/ci-doctor")
fmt.Println(" " + constants.CLIExtensionPrefix + " add githubnext/agentics/daily-plan@main")
}

return fmt.Errorf("workflow not found: %s", workflowPath)
}
Expand Down
207 changes: 207 additions & 0 deletions pkg/cli/add_workflow_not_found_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package cli

import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)

// TestDisplayAvailableWorkflows tests that displayAvailableWorkflows shows the list of available workflows
func TestDisplayAvailableWorkflows(t *testing.T) {
// Create a temporary packages directory structure
tempDir := t.TempDir()

// Override packages directory for testing
oldHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
defer os.Setenv("HOME", oldHome)

// Create a mock package structure
packagePath := filepath.Join(tempDir, ".aw", "packages", "test-owner", "test-repo")
workflowsDir := filepath.Join(packagePath, "workflows")
if err := os.MkdirAll(workflowsDir, 0755); err != nil {
t.Fatalf("Failed to create test directories: %v", err)
}

// Create some mock workflow files with valid frontmatter
validWorkflowContent := `---
on: push
---
# Test Workflow
`

workflows := []string{
"ci-doctor.md",
"daily-plan.md",
"weekly-summary.md",
}

for _, wf := range workflows {
wfPath := filepath.Join(workflowsDir, wf)
if err := os.WriteFile(wfPath, []byte(validWorkflowContent), 0644); err != nil {
t.Fatalf("Failed to create workflow file %s: %v", wf, err)
}
}

// Capture stderr output
oldStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w

// Call displayAvailableWorkflows
err := displayAvailableWorkflows("test-owner/test-repo", "", false)

// Restore stderr and capture output
w.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
buf.ReadFrom(r)
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error check: The return value from buf.ReadFrom(r) is not checked. While this is unlikely to fail in tests, it's best practice to check errors, especially since other I/O operations in the same tests do check errors.

Suggested change
buf.ReadFrom(r)
if _, err := buf.ReadFrom(r); err != nil {
t.Fatalf("Failed to read from pipe: %v", err)
}

Copilot uses AI. Check for mistakes.
output := buf.String()

if err != nil {
t.Errorf("displayAvailableWorkflows() unexpected error: %v", err)
}

// Check that the output contains the expected workflow names
expectedWorkflows := []string{"ci-doctor", "daily-plan", "weekly-summary"}
for _, wf := range expectedWorkflows {
if !strings.Contains(output, wf) {
t.Errorf("displayAvailableWorkflows() output should contain workflow '%s', got:\n%s", wf, output)
}
}

// Check that the output contains helpful information
if !strings.Contains(output, "Available workflows") {
t.Errorf("displayAvailableWorkflows() output should contain 'Available workflows', got:\n%s", output)
}

if !strings.Contains(output, "Example:") {
t.Errorf("displayAvailableWorkflows() output should contain 'Example:', got:\n%s", output)
}
}

// TestDisplayAvailableWorkflowsWithVersion tests displayAvailableWorkflows with a version
func TestDisplayAvailableWorkflowsWithVersion(t *testing.T) {
// Create a temporary packages directory structure
tempDir := t.TempDir()

// Override packages directory for testing
oldHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
defer os.Setenv("HOME", oldHome)

// Create a mock package structure
packagePath := filepath.Join(tempDir, ".aw", "packages", "test-owner", "test-repo")
workflowsDir := filepath.Join(packagePath, "workflows")
if err := os.MkdirAll(workflowsDir, 0755); err != nil {
t.Fatalf("Failed to create test directories: %v", err)
}

// Create a mock workflow file
validWorkflowContent := `---
on: push
---
# Test Workflow
`
wfPath := filepath.Join(workflowsDir, "test-workflow.md")
if err := os.WriteFile(wfPath, []byte(validWorkflowContent), 0644); err != nil {
t.Fatalf("Failed to create workflow file: %v", err)
}

// Capture stderr output
oldStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w

// Call displayAvailableWorkflows with a version
err := displayAvailableWorkflows("test-owner/test-repo", "v1.0.0", false)

// Restore stderr and capture output
w.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
buf.ReadFrom(r)
output := buf.String()

if err != nil {
t.Errorf("displayAvailableWorkflows() unexpected error: %v", err)
}

// Check that the version is included in the example command
if !strings.Contains(output, "@v1.0.0") {
t.Errorf("displayAvailableWorkflows() output should include version '@v1.0.0', got:\n%s", output)
}
}

// TestDisplayAvailableWorkflowsNoWorkflows tests when no workflows are found
func TestDisplayAvailableWorkflowsNoWorkflows(t *testing.T) {
// Create a temporary packages directory structure
tempDir := t.TempDir()

// Override packages directory for testing
oldHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
defer os.Setenv("HOME", oldHome)

// Create an empty package structure
packagePath := filepath.Join(tempDir, ".aw", "packages", "test-owner", "test-repo")
if err := os.MkdirAll(packagePath, 0755); err != nil {
t.Fatalf("Failed to create test directories: %v", err)
}

// Capture stderr output
oldStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w

// Call displayAvailableWorkflows
err := displayAvailableWorkflows("test-owner/test-repo", "", false)

// Restore stderr and capture output
w.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
buf.ReadFrom(r)
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error check: The return value from buf.ReadFrom(r) is not checked. While this is unlikely to fail in tests, it's best practice to check errors, especially since other I/O operations in the same tests do check errors.

Suggested change
buf.ReadFrom(r)
if _, err := buf.ReadFrom(r); err != nil {
t.Fatalf("Failed to read from pipe: %v", err)
}

Copilot uses AI. Check for mistakes.
output := buf.String()

if err != nil {
t.Errorf("displayAvailableWorkflows() unexpected error: %v", err)
}

// Check that the output contains a warning about no workflows
if !strings.Contains(output, "No workflows found") {
t.Errorf("displayAvailableWorkflows() output should contain 'No workflows found', got:\n%s", output)
}
}

// TestDisplayAvailableWorkflowsPackageNotFound tests when package is not found
func TestDisplayAvailableWorkflowsPackageNotFound(t *testing.T) {
// Create a temporary packages directory
tempDir := t.TempDir()

// Override packages directory for testing
oldHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
defer os.Setenv("HOME", oldHome)

// Create packages directory but don't create the specific package
packagesDir := filepath.Join(tempDir, ".aw", "packages")
if err := os.MkdirAll(packagesDir, 0755); err != nil {
t.Fatalf("Failed to create packages directory: %v", err)
}

// Call displayAvailableWorkflows with non-existent package
err := displayAvailableWorkflows("nonexistent/repo", "", false)

if err == nil {
t.Error("displayAvailableWorkflows() expected error for non-existent package, got nil")
}

if !strings.Contains(err.Error(), "package not found") {
t.Errorf("displayAvailableWorkflows() error should contain 'package not found', got: %v", err)
}
}
Loading