Skip to content

Commit d116152

Browse files
authored
Add actionable suggestions to "workflow not found" errors (#3896)
1 parent ec47a73 commit d116152

12 files changed

Lines changed: 195 additions & 19 deletions

File tree

cmd/gh-aw/main.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"fmt"
55
"os"
6+
"strings"
67

78
"github.com/githubnext/gh-aw/pkg/cli"
89
"github.com/githubnext/gh-aw/pkg/console"
@@ -189,7 +190,13 @@ Examples:
189190
JSONOutput: jsonOutput,
190191
}
191192
if _, err := cli.CompileWorkflows(config); err != nil {
192-
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
193+
errMsg := err.Error()
194+
// Check if error is already formatted (contains suggestions or starts with ✗)
195+
if strings.Contains(errMsg, "Suggestions:") || strings.HasPrefix(errMsg, "✗") {
196+
fmt.Fprintln(os.Stderr, errMsg)
197+
} else {
198+
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(errMsg))
199+
}
193200
os.Exit(1)
194201
}
195202
},

pkg/cli/commands.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package cli
22

33
import (
44
_ "embed"
5+
"errors"
56
"fmt"
67
"os"
78
"os/exec"
89
"path/filepath"
910
"strings"
1011

12+
"github.com/githubnext/gh-aw/pkg/console"
1113
"github.com/githubnext/gh-aw/pkg/constants"
1214
"github.com/githubnext/gh-aw/pkg/logger"
1315
)
@@ -79,10 +81,18 @@ func resolveWorkflowFile(fileOrWorkflowName string, verbose bool) (string, error
7981
// Try to find the workflow in local sources only (not packages)
8082
_, path, err := readWorkflowFile(workflowPath, workflowsDir)
8183
if err != nil {
82-
return "", fmt.Errorf("workflow '%s' not found in local .github/workflows or components", fileOrWorkflowName)
84+
suggestions := []string{
85+
fmt.Sprintf("Run '%s status' to see all available workflows", constants.CLIExtensionPrefix),
86+
fmt.Sprintf("Create a new workflow with '%s new %s'", constants.CLIExtensionPrefix, fileOrWorkflowName),
87+
"Check for typos in the workflow name",
88+
}
89+
return "", errors.New(console.FormatErrorWithSuggestions(
90+
fmt.Sprintf("workflow '%s' not found in local .github/workflows", fileOrWorkflowName),
91+
suggestions,
92+
))
8393
}
8494

85-
commandsLog.Print("Found workflow in local components")
95+
commandsLog.Print("Found workflow in local .github/workflows")
8696

8797
// Return absolute path
8898
absPath, err := filepath.Abs(path)

pkg/cli/compile_command.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,9 +326,9 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) {
326326
compileLog.Printf("Resolving workflow file: %s", markdownFile)
327327
resolvedFile, err := resolveWorkflowFile(markdownFile, verbose)
328328
if err != nil {
329-
errMsg := fmt.Sprintf("failed to resolve workflow '%s': %v", markdownFile, err)
330329
if !jsonOutput {
331-
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(errMsg))
330+
// Print the error directly - it already contains suggestions and formatting
331+
fmt.Fprintln(os.Stderr, err.Error())
332332
}
333333
errorMessages = append(errorMessages, err.Error())
334334
errorCount++

pkg/cli/enable.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package cli
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
67
"os/exec"
78
"path/filepath"
89
"strconv"
910
"strings"
11+
12+
"github.com/githubnext/gh-aw/pkg/console"
13+
"github.com/githubnext/gh-aw/pkg/constants"
1014
)
1115

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

168172
// Report any workflows that weren't found
169173
if len(notFoundNames) > 0 {
170-
return fmt.Errorf("workflows not found: %s", strings.Join(notFoundNames, ", "))
174+
suggestions := []string{
175+
fmt.Sprintf("Run '%s status' to see all available workflows", constants.CLIExtensionPrefix),
176+
"Check for typos in the workflow names",
177+
"Ensure the workflows have been compiled and pushed to GitHub",
178+
}
179+
return errors.New(console.FormatErrorWithSuggestions(
180+
fmt.Sprintf("workflows not found: %s", strings.Join(notFoundNames, ", ")),
181+
suggestions,
182+
))
171183
}
172184

173185
// If no targets after filtering, everything was already in the desired state

pkg/cli/logs.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,10 +357,15 @@ Examples:
357357
workflowName = args[0]
358358
} else {
359359
// Neither workflow ID nor valid GitHub Actions workflow name
360-
fmt.Fprintln(os.Stderr, console.FormatError(console.CompilerError{
361-
Type: "error",
362-
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),
363-
}))
360+
suggestions := []string{
361+
fmt.Sprintf("Run '%s status' to see all available workflows", constants.CLIExtensionPrefix),
362+
"Check for typos in the workflow name",
363+
"Use the workflow ID (e.g., 'test-claude') or GitHub Actions workflow name (e.g., 'Test Claude')",
364+
}
365+
fmt.Fprintln(os.Stderr, console.FormatErrorWithSuggestions(
366+
fmt.Sprintf("workflow '%s' not found", args[0]),
367+
suggestions,
368+
))
364369
os.Exit(1)
365370
}
366371
} else {

pkg/cli/resolver.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package cli
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
67
"path/filepath"
78
"strings"
89

10+
"github.com/githubnext/gh-aw/pkg/console"
11+
"github.com/githubnext/gh-aw/pkg/constants"
912
"github.com/githubnext/gh-aw/pkg/logger"
1013
)
1114

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

4144
// No matches found
4245
resolverLog.Printf("Workflow file not found: %s", workflowPath)
43-
return "", fmt.Errorf("workflow file not found: %s", workflowPath)
46+
suggestions := []string{
47+
fmt.Sprintf("Run '%s status' to see all available workflows", constants.CLIExtensionPrefix),
48+
"Check for typos in the workflow name",
49+
"Ensure the workflow file exists in .github/workflows/",
50+
}
51+
return "", errors.New(console.FormatErrorWithSuggestions(
52+
fmt.Sprintf("workflow file not found: %s", workflowPath),
53+
suggestions,
54+
))
4455
}
4556

4657
// NormalizeWorkflowFile normalizes a workflow file name by adding .md extension if missing

pkg/cli/run_command.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
67
"os"
78
"os/exec"
@@ -112,7 +113,7 @@ func RunWorkflowOnGitHub(workflowIdOrName string, enable bool, engineOverride st
112113

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

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

@@ -667,5 +675,13 @@ func validateRemoteWorkflow(workflowName string, repoOverride string, verbose bo
667675
}
668676
}
669677

670-
return fmt.Errorf("workflow '%s' not found in repository '%s'", lockFileName, repoOverride)
678+
suggestions := []string{
679+
"Check if the workflow has been pushed to the remote repository",
680+
"Verify the workflow file exists in the repository's .github/workflows directory",
681+
fmt.Sprintf("Run '%s status' to see available workflows", constants.CLIExtensionPrefix),
682+
}
683+
return errors.New(console.FormatErrorWithSuggestions(
684+
fmt.Sprintf("workflow '%s' not found in repository '%s'", lockFileName, repoOverride),
685+
suggestions,
686+
))
671687
}

pkg/cli/workflows.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
67
"os"
78
"os/exec"
@@ -10,6 +11,7 @@ import (
1011
"strings"
1112

1213
"github.com/githubnext/gh-aw/pkg/console"
14+
"github.com/githubnext/gh-aw/pkg/constants"
1315
"github.com/githubnext/gh-aw/pkg/logger"
1416
)
1517

@@ -134,7 +136,15 @@ func getWorkflowStatus(workflowIdOrName string, repoOverride string, verbose boo
134136
return workflow, nil
135137
}
136138

137-
return nil, fmt.Errorf("workflow '%s' not found on GitHub", workflowIdOrName)
139+
suggestions := []string{
140+
fmt.Sprintf("Run '%s status' to see all available workflows", constants.CLIExtensionPrefix),
141+
"Check if the workflow has been compiled and pushed to GitHub",
142+
"Verify the workflow name matches the compiled .lock.yml file",
143+
}
144+
return nil, errors.New(console.FormatErrorWithSuggestions(
145+
fmt.Sprintf("workflow '%s' not found on GitHub", workflowIdOrName),
146+
suggestions,
147+
))
138148
}
139149

140150
// restoreWorkflowState restores a workflow to disabled state if it was previously disabled

pkg/console/console.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,21 @@ func FormatErrorMessage(message string) string {
435435
return applyStyle(errorStyle, "✗ ") + message
436436
}
437437

438+
// FormatErrorWithSuggestions formats an error message with actionable suggestions
439+
func FormatErrorWithSuggestions(message string, suggestions []string) string {
440+
var output strings.Builder
441+
output.WriteString(FormatErrorMessage(message))
442+
443+
if len(suggestions) > 0 {
444+
output.WriteString("\n\nSuggestions:\n")
445+
for _, suggestion := range suggestions {
446+
output.WriteString(" • " + suggestion + "\n")
447+
}
448+
}
449+
450+
return output.String()
451+
}
452+
438453
// RenderTableAsJSON renders a table configuration as JSON
439454
// This converts the table structure to a JSON array of objects
440455
func RenderTableAsJSON(config TableConfig) (string, error) {

pkg/console/console_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,72 @@ func TestFormatError(t *testing.T) {
8888
}
8989
}
9090

91+
func TestFormatErrorWithSuggestions(t *testing.T) {
92+
tests := []struct {
93+
name string
94+
message string
95+
suggestions []string
96+
expected []string
97+
}{
98+
{
99+
name: "error with suggestions",
100+
message: "workflow 'test' not found",
101+
suggestions: []string{
102+
"Run 'gh aw status' to see all available workflows",
103+
"Create a new workflow with 'gh aw new test'",
104+
"Check for typos in the workflow name",
105+
},
106+
expected: []string{
107+
"✗",
108+
"workflow 'test' not found",
109+
"Suggestions:",
110+
"• Run 'gh aw status' to see all available workflows",
111+
"• Create a new workflow with 'gh aw new test'",
112+
"• Check for typos in the workflow name",
113+
},
114+
},
115+
{
116+
name: "error without suggestions",
117+
message: "workflow 'test' not found",
118+
suggestions: []string{},
119+
expected: []string{
120+
"✗",
121+
"workflow 'test' not found",
122+
},
123+
},
124+
{
125+
name: "error with single suggestion",
126+
message: "file not found",
127+
suggestions: []string{
128+
"Check the file path",
129+
},
130+
expected: []string{
131+
"✗",
132+
"file not found",
133+
"Suggestions:",
134+
"• Check the file path",
135+
},
136+
},
137+
}
138+
139+
for _, tt := range tests {
140+
t.Run(tt.name, func(t *testing.T) {
141+
output := FormatErrorWithSuggestions(tt.message, tt.suggestions)
142+
143+
for _, expected := range tt.expected {
144+
if !strings.Contains(output, expected) {
145+
t.Errorf("Expected output to contain '%s', but got:\n%s", expected, output)
146+
}
147+
}
148+
149+
// Verify no suggestions section when empty
150+
if len(tt.suggestions) == 0 && strings.Contains(output, "Suggestions:") {
151+
t.Errorf("Expected no suggestions section for empty suggestions, got:\n%s", output)
152+
}
153+
})
154+
}
155+
}
156+
91157
func TestFormatSuccessMessage(t *testing.T) {
92158
output := FormatSuccessMessage("compilation completed")
93159
if !strings.Contains(output, "compilation completed") {

0 commit comments

Comments
 (0)