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
21 changes: 12 additions & 9 deletions cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,12 +319,9 @@ Examples:
FailFast: failFast,
}
if _, err := cli.CompileWorkflows(cmd.Context(), config); err != nil {
// Format validation error for better user experience
// The main function will detect the ✗ prefix and print it without double formatting
if !jsonOutput {
// Return a new error with formatted message so main() can print it
return fmt.Errorf("%s", cli.FormatValidationError(err))
}
// Return error as-is without additional formatting
// Errors from CompileWorkflows are already formatted with console.FormatError
// which provides IDE-parseable location information (file:line:column)
return err
}
return nil
Expand Down Expand Up @@ -685,9 +682,15 @@ func main() {

if err := rootCmd.Execute(); err != nil {
errMsg := err.Error()
// Check if error is already formatted (contains suggestions or starts with ✗)
// to avoid double formatting with FormatErrorMessage
if strings.Contains(errMsg, "Suggestions:") || strings.HasPrefix(errMsg, "✗") {
// Check if error is already formatted to avoid double formatting:
// - Contains suggestions (FormatErrorWithSuggestions)
// - Starts with ✗ (FormatErrorMessage)
// - Contains file:line:column: pattern (console.FormatError)
isAlreadyFormatted := strings.Contains(errMsg, "Suggestions:") ||
strings.HasPrefix(errMsg, "✗") ||
strings.Contains(errMsg, ":") && (strings.Contains(errMsg, "error:") || strings.Contains(errMsg, "warning:"))

if isAlreadyFormatted {
fmt.Fprintln(os.Stderr, errMsg)
} else {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(errMsg))
Expand Down
3 changes: 2 additions & 1 deletion pkg/cli/compile_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ func compileSingleFile(compiler *workflow.Compiler, file string, stats *Compilat

if err := CompileWorkflowWithValidation(compiler, file, verbose, false, false, false, false, false); err != nil {
// Always show compilation errors on new line
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
// Note: Don't wrap in FormatErrorMessage as the error is already formatted by console.FormatError
fmt.Fprintln(os.Stderr, err.Error())
stats.Errors++
Comment on lines 101 to 105
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

compileSingleFile now prints err.Error() directly. CompileWorkflowWithValidation can return plain/unformatted errors (e.g. lockfile YAML validation failures or security-scan wrapper errors in pkg/cli/compile_validation.go), so this is a UX regression vs the previous console.FormatErrorMessage(...) behavior. Suggest conditionally formatting only when the message isn't already in console.FormatError/suggestions/✗ form (similar to main()), or format these non-console.FormatError errors at the source before returning.

Copilot uses AI. Check for mistakes.
stats.FailedWorkflows = append(stats.FailedWorkflows, filepath.Base(file))
} else {
Expand Down
6 changes: 6 additions & 0 deletions pkg/console/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func applyStyle(style lipgloss.Style, text string) string {
}

// ToRelativePath converts an absolute path to a relative path from the current working directory
// If the relative path contains "..", returns the absolute path instead for clarity
func ToRelativePath(path string) string {
if !filepath.IsAbs(path) {
return path
Expand All @@ -64,6 +65,11 @@ func ToRelativePath(path string) string {
return path
}

// If the relative path contains "..", use absolute path instead for clarity
if strings.Contains(relPath, "..") {
return path
}
Comment on lines +68 to +71
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

ToRelativePath checks strings.Contains(relPath, ".."), which will also match legitimate directory/file names that include .. (e.g. some..dir/file.md) and incorrectly force an absolute path. Consider detecting .. as a path segment instead (split on filepath.Separator / use filepath.Clean + component scan), or check for patterns like .. at segment boundaries (relPath == "..", prefix ".."+sep, or contains sep+".."+sep).

Copilot uses AI. Check for mistakes.

return relPath
}

Expand Down
33 changes: 26 additions & 7 deletions pkg/console/console_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package console

import (
"os"
"path/filepath"
"strings"
"testing"
Expand Down Expand Up @@ -298,11 +299,28 @@ func TestToRelativePath(t *testing.T) {
},
},
{
name: "absolute path converted to relative",
name: "absolute path with .. in relative form returns absolute",
path: "/tmp/gh-aw/test.md",
expectedFunc: func(result, expected string) bool {
// Should be a relative path that doesn't start with /
return !strings.HasPrefix(result, "/") && strings.HasSuffix(result, "test.md")
// When relative path would contain "..", should return absolute path
// The relative path from /home/runner/work/gh-aw/gh-aw to /tmp/gh-aw/test.md
// would be ../../../../../tmp/gh-aw/test.md (contains ..)
// So we should get the absolute path back
return result == "/tmp/gh-aw/test.md"
},
},
{
name: "absolute path within working directory converted to relative",
path: func() string {
// Get current working directory and construct a path within it
wd, _ := os.Getwd()
return filepath.Join(wd, "pkg/console/test.md")
Comment on lines +315 to +317
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The test builds an absolute path from os.Getwd() but ignores the returned error. If Getwd() fails in some environments, this will silently generate a bad path and can make the test behave unpredictably; please handle the error (e.g., require.NoError(t, err) / t.Fatalf(...)).

Copilot uses AI. Check for mistakes.
}(),
expectedFunc: func(result, expected string) bool {
// Absolute path within working directory should be converted to relative
// without ".." in the path
// The result should not start with / and should not contain ..
return !strings.HasPrefix(result, "/") && !strings.Contains(result, "..") && strings.HasSuffix(result, "test.md")
Comment on lines +320 to +323
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

These assertions infer “relative path” by checking !strings.HasPrefix(result, "/"), which is not valid on Windows (absolute paths typically start with a drive letter, so an absolute path could still pass). Since CI runs windows-latest, use filepath.IsAbs(result) and/or validate the absence of .. segments after filepath.Clean instead of relying on / prefixes.

This issue also appears on line 360 of the same file.

Suggested change
// Absolute path within working directory should be converted to relative
// without ".." in the path
// The result should not start with / and should not contain ..
return !strings.HasPrefix(result, "/") && !strings.Contains(result, "..") && strings.HasSuffix(result, "test.md")
// Absolute path within working directory should be converted to a relative path
// that does not traverse upwards ("..") and is not absolute on any platform.
cleaned := filepath.Clean(result)
return !filepath.IsAbs(result) && !strings.Contains(cleaned, "..") && strings.HasSuffix(cleaned, "test.md")

Copilot uses AI. Check for mistakes.
},
},
}
Expand Down Expand Up @@ -336,13 +354,14 @@ func TestFormatErrorWithAbsolutePaths(t *testing.T) {

// The output should contain test.md and line:column information
if !strings.Contains(output, "test.md:5:10:") {
t.Errorf("Expected output to contain relative file path with line:column, got: %s", output)
t.Errorf("Expected output to contain file path with line:column, got: %s", output)
}

// The output should not start with an absolute path (no leading /)
// Since tmpDir is outside the working directory (in /tmp), the path should be absolute
// to avoid confusing relative paths with ".." components
lines := strings.Split(output, "\n")
if strings.HasPrefix(lines[0], "/") {
t.Errorf("Expected output to start with relative path, but found absolute path: %s", lines[0])
if !strings.HasPrefix(lines[0], "/") {
t.Errorf("Expected output to start with absolute path for files outside working directory, got: %s", lines[0])
}

// Should contain error message
Expand Down
6 changes: 3 additions & 3 deletions pkg/parser/json_path_locator.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,12 +398,12 @@ func findNestedSection(yamlContent string, pathSegments []PathSegment) NestedSec
if segment.Type == "key" {
// Look for "key:" pattern
keyPattern := regexp.MustCompile(`^` + regexp.QuoteMeta(segment.Value) + `\s*:`)
if keyPattern.MatchString(trimmedLine) && lineLevel == currentLevel*2 {
if keyPattern.MatchString(trimmedLine) && lineLevel == currentLevel {
// Found a matching key at the correct indentation level
if currentLevel == len(pathSegments)-1 {
// This is the final segment - we found our target
foundLine = lineNum
baseIndentLevel = lineLevel + 2 // Properties inside this object should be indented further
baseIndentLevel = lineLevel + 1 // Properties inside this object should be indented one level further
break
} else {
// Move to the next level
Expand All @@ -420,7 +420,7 @@ func findNestedSection(yamlContent string, pathSegments []PathSegment) NestedSec

// Find the end of this nested section by looking for the next line at the same or lower indentation
endLine := len(lines) - 1 // Default to end of file
targetLevel := baseIndentLevel - 2 // The level of the key we found
targetLevel := baseIndentLevel - 1 // The level of the key we found

for lineNum := foundLine + 1; lineNum < len(lines); lineNum++ {
line := lines[lineNum]
Expand Down
Loading
Loading