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
9 changes: 8 additions & 1 deletion cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"strings"

"github.com/githubnext/gh-aw/pkg/cli"
"github.com/githubnext/gh-aw/pkg/console"
Expand Down Expand Up @@ -189,7 +190,13 @@ Examples:
JSONOutput: jsonOutput,
}
if _, err := cli.CompileWorkflows(config); err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
errMsg := err.Error()
// Check if error is already formatted (contains suggestions or starts with ✗)
if strings.Contains(errMsg, "Suggestions:") || strings.HasPrefix(errMsg, "✗") {
fmt.Fprintln(os.Stderr, errMsg)
} else {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(errMsg))
}
os.Exit(1)
}
},
Expand Down
14 changes: 12 additions & 2 deletions pkg/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package cli

import (
_ "embed"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/githubnext/gh-aw/pkg/console"
"github.com/githubnext/gh-aw/pkg/constants"
"github.com/githubnext/gh-aw/pkg/logger"
)
Expand Down Expand Up @@ -79,10 +81,18 @@ func resolveWorkflowFile(fileOrWorkflowName string, verbose bool) (string, error
// Try to find the workflow in local sources only (not packages)
_, path, err := readWorkflowFile(workflowPath, workflowsDir)
if err != nil {
return "", fmt.Errorf("workflow '%s' not found in local .github/workflows or components", fileOrWorkflowName)
suggestions := []string{
fmt.Sprintf("Run '%s status' to see all available workflows", constants.CLIExtensionPrefix),
fmt.Sprintf("Create a new workflow with '%s new %s'", constants.CLIExtensionPrefix, fileOrWorkflowName),
"Check for typos in the workflow name",
}
return "", errors.New(console.FormatErrorWithSuggestions(
fmt.Sprintf("workflow '%s' not found in local .github/workflows", fileOrWorkflowName),
suggestions,
))
}

commandsLog.Print("Found workflow in local components")
commandsLog.Print("Found workflow in local .github/workflows")

// Return absolute path
absPath, err := filepath.Abs(path)
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/compile_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,9 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) {
compileLog.Printf("Resolving workflow file: %s", markdownFile)
resolvedFile, err := resolveWorkflowFile(markdownFile, verbose)
if err != nil {
errMsg := fmt.Sprintf("failed to resolve workflow '%s': %v", markdownFile, err)
if !jsonOutput {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(errMsg))
// Print the error directly - it already contains suggestions and formatting
fmt.Fprintln(os.Stderr, err.Error())
}
errorMessages = append(errorMessages, err.Error())
errorCount++
Expand Down
14 changes: 13 additions & 1 deletion pkg/cli/enable.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package cli

import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"

"github.com/githubnext/gh-aw/pkg/console"
"github.com/githubnext/gh-aw/pkg/constants"
)

// EnableWorkflowsByNames enables workflows by specific names, or all if no names provided
Expand Down Expand Up @@ -167,7 +171,15 @@ func toggleWorkflowsByNames(workflowNames []string, enable bool) error {

// Report any workflows that weren't found
if len(notFoundNames) > 0 {
return fmt.Errorf("workflows not found: %s", strings.Join(notFoundNames, ", "))
suggestions := []string{
fmt.Sprintf("Run '%s status' to see all available workflows", constants.CLIExtensionPrefix),
"Check for typos in the workflow names",
"Ensure the workflows have been compiled and pushed to GitHub",
}
return errors.New(console.FormatErrorWithSuggestions(
fmt.Sprintf("workflows not found: %s", strings.Join(notFoundNames, ", ")),
suggestions,
))
}

// If no targets after filtering, everything was already in the desired state
Expand Down
13 changes: 9 additions & 4 deletions pkg/cli/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,10 +357,15 @@ Examples:
workflowName = args[0]
} else {
// Neither workflow ID nor valid GitHub Actions workflow name
fmt.Fprintln(os.Stderr, console.FormatError(console.CompilerError{
Type: "error",
Message: fmt.Sprintf("workflow '%s' not found. Expected either a workflow ID (e.g., 'test-claude') or GitHub Actions workflow name (e.g., 'Test Claude'). Original error: %v", args[0], err),
}))
suggestions := []string{
fmt.Sprintf("Run '%s status' to see all available workflows", constants.CLIExtensionPrefix),
"Check for typos in the workflow name",
"Use the workflow ID (e.g., 'test-claude') or GitHub Actions workflow name (e.g., 'Test Claude')",
}
fmt.Fprintln(os.Stderr, console.FormatErrorWithSuggestions(
fmt.Sprintf("workflow '%s' not found", args[0]),
suggestions,
))
os.Exit(1)
}
} else {
Expand Down
13 changes: 12 additions & 1 deletion pkg/cli/resolver.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package cli

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/githubnext/gh-aw/pkg/console"
"github.com/githubnext/gh-aw/pkg/constants"
"github.com/githubnext/gh-aw/pkg/logger"
)

Expand Down Expand Up @@ -40,7 +43,15 @@ func ResolveWorkflowPath(workflowFile string) (string, error) {

// No matches found
resolverLog.Printf("Workflow file not found: %s", workflowPath)
return "", fmt.Errorf("workflow file not found: %s", workflowPath)
suggestions := []string{
fmt.Sprintf("Run '%s status' to see all available workflows", constants.CLIExtensionPrefix),
"Check for typos in the workflow name",
"Ensure the workflow file exists in .github/workflows/",
}
return "", errors.New(console.FormatErrorWithSuggestions(
fmt.Sprintf("workflow file not found: %s", workflowPath),
suggestions,
))
}

// NormalizeWorkflowFile normalizes a workflow file name by adding .md extension if missing
Expand Down
22 changes: 19 additions & 3 deletions pkg/cli/run_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -112,7 +113,7 @@ func RunWorkflowOnGitHub(workflowIdOrName string, enable bool, engineOverride st

_, _, err := readWorkflowFile(workflowIdOrName+".md", workflowsDir)
if err != nil {
return fmt.Errorf("failed to find workflow in local .github/workflows or components: %w", err)
return fmt.Errorf("failed to find workflow in local .github/workflows: %w", err)
}

// For local workflows, use the simple filename
Expand All @@ -122,7 +123,14 @@ func RunWorkflowOnGitHub(workflowIdOrName string, enable bool, engineOverride st
// Check if the lock file exists in .github/workflows
lockFilePath = filepath.Join(".github/workflows", lockFileName)
if _, err := os.Stat(lockFilePath); os.IsNotExist(err) {
return fmt.Errorf("workflow lock file '%s' not found in .github/workflows - run '"+constants.CLIExtensionPrefix+" compile' first", lockFileName)
suggestions := []string{
fmt.Sprintf("Run '%s compile' to compile all workflows", constants.CLIExtensionPrefix),
fmt.Sprintf("Run '%s compile %s' to compile this specific workflow", constants.CLIExtensionPrefix, filename),
}
return errors.New(console.FormatErrorWithSuggestions(
fmt.Sprintf("workflow lock file '%s' not found in .github/workflows", lockFileName),
suggestions,
))
}
}

Expand Down Expand Up @@ -667,5 +675,13 @@ func validateRemoteWorkflow(workflowName string, repoOverride string, verbose bo
}
}

return fmt.Errorf("workflow '%s' not found in repository '%s'", lockFileName, repoOverride)
suggestions := []string{
"Check if the workflow has been pushed to the remote repository",
"Verify the workflow file exists in the repository's .github/workflows directory",
fmt.Sprintf("Run '%s status' to see available workflows", constants.CLIExtensionPrefix),
}
return errors.New(console.FormatErrorWithSuggestions(
fmt.Sprintf("workflow '%s' not found in repository '%s'", lockFileName, repoOverride),
suggestions,
))
}
12 changes: 11 additions & 1 deletion pkg/cli/workflows.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
Expand All @@ -10,6 +11,7 @@ import (
"strings"

"github.com/githubnext/gh-aw/pkg/console"
"github.com/githubnext/gh-aw/pkg/constants"
"github.com/githubnext/gh-aw/pkg/logger"
)

Expand Down Expand Up @@ -134,7 +136,15 @@ func getWorkflowStatus(workflowIdOrName string, repoOverride string, verbose boo
return workflow, nil
}

return nil, fmt.Errorf("workflow '%s' not found on GitHub", workflowIdOrName)
suggestions := []string{
fmt.Sprintf("Run '%s status' to see all available workflows", constants.CLIExtensionPrefix),
"Check if the workflow has been compiled and pushed to GitHub",
"Verify the workflow name matches the compiled .lock.yml file",
}
return nil, errors.New(console.FormatErrorWithSuggestions(
fmt.Sprintf("workflow '%s' not found on GitHub", workflowIdOrName),
suggestions,
))
}

// restoreWorkflowState restores a workflow to disabled state if it was previously disabled
Expand Down
15 changes: 15 additions & 0 deletions pkg/console/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,21 @@ func FormatErrorMessage(message string) string {
return applyStyle(errorStyle, "✗ ") + message
}

// FormatErrorWithSuggestions formats an error message with actionable suggestions
func FormatErrorWithSuggestions(message string, suggestions []string) string {
var output strings.Builder
output.WriteString(FormatErrorMessage(message))

if len(suggestions) > 0 {
output.WriteString("\n\nSuggestions:\n")
for _, suggestion := range suggestions {
output.WriteString(" • " + suggestion + "\n")
}
Comment on lines +445 to +447
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

[nitpick] The trailing newline in the formatted error with suggestions output may cause inconsistent spacing. When this error is later printed with fmt.Fprintln(), it will add an additional newline, resulting in a blank line after the suggestions section. Consider removing the trailing \n on line 447 to maintain consistent spacing.

Suggested change
for _, suggestion := range suggestions {
output.WriteString(" • " + suggestion + "\n")
}
formatted := make([]string, len(suggestions))
for i, suggestion := range suggestions {
formatted[i] = " • " + suggestion
}
output.WriteString(strings.Join(formatted, "\n"))

Copilot uses AI. Check for mistakes.
}

return output.String()
}

// RenderTableAsJSON renders a table configuration as JSON
// This converts the table structure to a JSON array of objects
func RenderTableAsJSON(config TableConfig) (string, error) {
Expand Down
66 changes: 66 additions & 0 deletions pkg/console/console_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,72 @@ func TestFormatError(t *testing.T) {
}
}

func TestFormatErrorWithSuggestions(t *testing.T) {
tests := []struct {
name string
message string
suggestions []string
expected []string
}{
{
name: "error with suggestions",
message: "workflow 'test' not found",
suggestions: []string{
"Run 'gh aw status' to see all available workflows",
"Create a new workflow with 'gh aw new test'",
"Check for typos in the workflow name",
},
expected: []string{
"✗",
"workflow 'test' not found",
"Suggestions:",
"• Run 'gh aw status' to see all available workflows",
"• Create a new workflow with 'gh aw new test'",
"• Check for typos in the workflow name",
},
},
{
name: "error without suggestions",
message: "workflow 'test' not found",
suggestions: []string{},
expected: []string{
"✗",
"workflow 'test' not found",
},
},
{
name: "error with single suggestion",
message: "file not found",
suggestions: []string{
"Check the file path",
},
expected: []string{
"✗",
"file not found",
"Suggestions:",
"• Check the file path",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := FormatErrorWithSuggestions(tt.message, tt.suggestions)

for _, expected := range tt.expected {
if !strings.Contains(output, expected) {
t.Errorf("Expected output to contain '%s', but got:\n%s", expected, output)
}
}

// Verify no suggestions section when empty
if len(tt.suggestions) == 0 && strings.Contains(output, "Suggestions:") {
t.Errorf("Expected no suggestions section for empty suggestions, got:\n%s", output)
}
})
}
}

func TestFormatSuccessMessage(t *testing.T) {
output := FormatSuccessMessage("compilation completed")
if !strings.Contains(output, "compilation completed") {
Expand Down
30 changes: 27 additions & 3 deletions pkg/workflow/resolve.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package workflow

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/githubnext/gh-aw/pkg/console"
"github.com/githubnext/gh-aw/pkg/constants"
"github.com/githubnext/gh-aw/pkg/logger"
"github.com/goccy/go-yaml"
Expand Down Expand Up @@ -42,7 +44,15 @@ func ResolveWorkflowName(workflowInput string) (string, error) {
mdFile := filepath.Join(workflowsDir, normalizedName+".md")
if _, err := os.Stat(mdFile); err != nil {
resolveLog.Printf("Markdown file not found: %s", mdFile)
return "", fmt.Errorf("agentic workflow '%s' not found (expected file: %s)", normalizedName, mdFile)
suggestions := []string{
fmt.Sprintf("Run '%s status' to see all available workflows", constants.CLIExtensionPrefix),
fmt.Sprintf("Create a new workflow with '%s new %s'", constants.CLIExtensionPrefix, normalizedName),
"Check for typos in the workflow name",
}
return "", errors.New(console.FormatErrorWithSuggestions(
fmt.Sprintf("agentic workflow '%s' not found (expected file: %s)", normalizedName, mdFile),
suggestions,
))
}

// The corresponding lock file name is what GitHub Actions uses as the workflow name
Expand All @@ -51,7 +61,14 @@ func ResolveWorkflowName(workflowInput string) (string, error) {

// Check if the lock file exists (should be generated by compile)
if _, err := os.Stat(lockFile); err != nil {
return "", fmt.Errorf("compiled workflow '%s' not found (expected file: %s). Run 'gh aw compile' first", normalizedName, lockFile)
suggestions := []string{
fmt.Sprintf("Run '%s compile' to compile the workflow", constants.CLIExtensionPrefix),
fmt.Sprintf("Run '%s compile %s' to compile this specific workflow", constants.CLIExtensionPrefix, normalizedName),
}
return "", errors.New(console.FormatErrorWithSuggestions(
fmt.Sprintf("compiled workflow '%s' not found (expected file: %s)", normalizedName, lockFile),
suggestions,
))
}

// Read and parse the lock file to extract the workflow name
Expand All @@ -71,7 +88,14 @@ func ResolveWorkflowName(workflowInput string) (string, error) {

if workflow.Name == "" {
resolveLog.Printf("Workflow name field missing in lock file: %s", lockFile)
return "", fmt.Errorf("workflow name not found in lock file '%s'", lockFile)
suggestions := []string{
fmt.Sprintf("Run '%s compile %s' to recompile the workflow", constants.CLIExtensionPrefix, normalizedName),
"Check if the workflow file has a valid 'name' field in the frontmatter",
}
return "", errors.New(console.FormatErrorWithSuggestions(
fmt.Sprintf("workflow name not found in lock file '%s'", lockFile),
suggestions,
))
}

resolveLog.Printf("Successfully resolved workflow name: %s", workflow.Name)
Expand Down
Loading
Loading