From 883621be2299cd3c00db1d2a25da05b599a866ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:27:15 +0000 Subject: [PATCH 1/3] Initial plan From 59ca3c21b060b350c59e8640a23bcab9665dc2c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:35:37 +0000 Subject: [PATCH 2/3] fix(parser): include per-field locations for safe-output schema failures with additional tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../safe_outputs_error_location_test.go | 122 ++++++++++++++++++ pkg/parser/schema_compiler.go | 39 ++++-- 2 files changed, 152 insertions(+), 9 deletions(-) diff --git a/pkg/parser/safe_outputs_error_location_test.go b/pkg/parser/safe_outputs_error_location_test.go index 47d667dd9a..89b8209c07 100644 --- a/pkg/parser/safe_outputs_error_location_test.go +++ b/pkg/parser/safe_outputs_error_location_test.go @@ -3,6 +3,9 @@ package parser import ( + "os" + "path/filepath" + "strings" "testing" ) @@ -385,3 +388,122 @@ safe-outputs: }) } } + +func TestValidateWithSchemaAndLocationReportsAllSafeOutputFailures(t *testing.T) { + t.Parallel() + + yamlContent := `--- +on: daily +safe-outputs: + create-issue: + invalid-issue-field: true + create-discussion: + invalid-discussion-field: true +--- +# body` + filePath := filepath.Join(t.TempDir(), "workflow.md") + if err := os.WriteFile(filePath, []byte(yamlContent), 0644); err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + frontmatter := map[string]any{ + "on": "daily", + "safe-outputs": map[string]any{ + "create-issue": map[string]any{ + "invalid-issue-field": true, + }, + "create-discussion": map[string]any{ + "invalid-discussion-field": true, + }, + }, + } + + err := validateWithSchemaAndLocation(frontmatter, mainWorkflowSchema, "main workflow file", filePath) + if err == nil { + t.Fatal("expected schema validation error, got nil") + } + + errorText := err.Error() + wantSubstrings := []string{ + "/safe-outputs/create-issue", + "/safe-outputs/create-discussion", + "line 5, column 5", + "line 7, column 5", + } + for _, want := range wantSubstrings { + if !strings.Contains(errorText, want) { + t.Fatalf("expected error to contain %q, got:\n%s", want, errorText) + } + } +} + +// TestFormatSchemaFailureDetailEmptyPath verifies that an empty path is normalised to "/". +func TestFormatSchemaFailureDetailEmptyPath(t *testing.T) { + t.Parallel() + + pathInfo := JSONPathInfo{ + Path: "", + Message: "additional property 'x' not allowed", + } + result := formatSchemaFailureDetail(pathInfo, "on: daily\n", 1) + if !strings.HasPrefix(result, "at '/'") { + t.Errorf("expected result to start with \"at '/'\", got: %s", result) + } +} + +// TestFormatSchemaFailureDetailLineColumn verifies that line/column numbers are +// included in the formatted detail when the path can be located in YAML. +func TestFormatSchemaFailureDetailLineColumn(t *testing.T) { + t.Parallel() + + frontmatterContent := "on: daily\nsafe-outputs:\n create-issue:\n invalid-field: true\n" + pathInfo := JSONPathInfo{ + Path: "/safe-outputs/create-issue", + Message: "additional property 'invalid-field' not allowed", + } + result := formatSchemaFailureDetail(pathInfo, frontmatterContent, 1) + if !strings.Contains(result, "line ") || !strings.Contains(result, "column ") { + t.Errorf("expected result to contain line/column info, got: %s", result) + } + if !strings.Contains(result, "/safe-outputs/create-issue") { + t.Errorf("expected result to contain path '/safe-outputs/create-issue', got: %s", result) + } +} + +// TestValidateWithSchemaAndLocationSingleFailureNoBulletPrefix verifies that when +// only one schema failure occurs the error message does not include the +// "Multiple schema validation failures" prefix. +func TestValidateWithSchemaAndLocationSingleFailureNoBulletPrefix(t *testing.T) { + t.Parallel() + + yamlContent := `--- +on: daily +safe-outputs: + create-issue: + invalid-single-field: true +--- +# body` + filePath := filepath.Join(t.TempDir(), "workflow.md") + if err := os.WriteFile(filePath, []byte(yamlContent), 0644); err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + frontmatter := map[string]any{ + "on": "daily", + "safe-outputs": map[string]any{ + "create-issue": map[string]any{ + "invalid-single-field": true, + }, + }, + } + + err := validateWithSchemaAndLocation(frontmatter, mainWorkflowSchema, "main workflow file", filePath) + if err == nil { + t.Fatal("expected schema validation error, got nil") + } + + errorText := err.Error() + if strings.Contains(errorText, "Multiple schema validation failures") { + t.Errorf("single failure should not use 'Multiple schema validation failures' prefix; got:\n%s", errorText) + } +} diff --git a/pkg/parser/schema_compiler.go b/pkg/parser/schema_compiler.go index f50a3b3cfd..3c9c13bf03 100644 --- a/pkg/parser/schema_compiler.go +++ b/pkg/parser/schema_compiler.go @@ -244,7 +244,12 @@ func validateWithSchemaAndLocation(frontmatter map[string]any, schemaJSON, conte // If we have paths and frontmatter content, try to get precise locations if len(jsonPaths) > 0 && frontmatterContent != "" { - // Use the first error path for the primary error location + detailLines := make([]string, 0, len(jsonPaths)) + for _, pathInfo := range jsonPaths { + detailLines = append(detailLines, formatSchemaFailureDetail(pathInfo, frontmatterContent, frontmatterStart)) + } + + // Use the first error path for primary context rendering. primaryPath := jsonPaths[0] location := LocateJSONPathInYAMLWithAdditionalProperties(frontmatterContent, primaryPath.Path, primaryPath.Message) @@ -275,14 +280,12 @@ func validateWithSchemaAndLocation(frontmatter map[string]any, schemaJSON, conte adjustedContextLines = contextLines } - // Rewrite "additional properties not allowed" errors to be more friendly - // Also clean up oneOf jargon (e.g., "got string, want object") to plain English - message := rewriteAdditionalPropertiesError(cleanOneOfMessage(primaryPath.Message)) - - // Add schema-based suggestions - suggestions := generateSchemaBasedSuggestions(schemaJSON, primaryPath.Message, primaryPath.Path, frontmatterContent) - if suggestions != "" { - message = message + ". " + suggestions + // Include every schema failure with path + line + column. + message := "" + if len(detailLines) == 1 { + message = detailLines[0] + } else { + message = "Multiple schema validation failures:\n- " + strings.Join(detailLines, "\n- ") } // Create a compiler error with precise location information @@ -335,6 +338,24 @@ func validateWithSchemaAndLocation(frontmatter map[string]any, schemaJSON, conte return err } +func formatSchemaFailureDetail(pathInfo JSONPathInfo, frontmatterContent string, frontmatterStart int) string { + path := pathInfo.Path + if path == "" { + path = "/" + } + + location := LocateJSONPathInYAMLWithAdditionalProperties(frontmatterContent, pathInfo.Path, pathInfo.Message) + line := frontmatterStart + column := 1 + if location.Found { + line = location.Line + frontmatterStart - 1 + column = location.Column + } + + message := rewriteAdditionalPropertiesError(cleanOneOfMessage(pathInfo.Message)) + return fmt.Sprintf("at '%s' (line %d, column %d): %s", path, line, column, message) +} + // GetMainWorkflowSchema returns the embedded main workflow schema JSON func GetMainWorkflowSchema() string { return mainWorkflowSchema From 79715314559d6eb618e370096ab518a9638faa0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:48:11 +0000 Subject: [PATCH 3/3] fix(parser): merge main and restore schema suggestions in formatSchemaFailureDetail Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/safe_outputs_error_location_test.go | 4 ++-- pkg/parser/schema_compiler.go | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/parser/safe_outputs_error_location_test.go b/pkg/parser/safe_outputs_error_location_test.go index 89b8209c07..b65af60736 100644 --- a/pkg/parser/safe_outputs_error_location_test.go +++ b/pkg/parser/safe_outputs_error_location_test.go @@ -445,7 +445,7 @@ func TestFormatSchemaFailureDetailEmptyPath(t *testing.T) { Path: "", Message: "additional property 'x' not allowed", } - result := formatSchemaFailureDetail(pathInfo, "on: daily\n", 1) + result := formatSchemaFailureDetail(pathInfo, "", "on: daily\n", 1) if !strings.HasPrefix(result, "at '/'") { t.Errorf("expected result to start with \"at '/'\", got: %s", result) } @@ -461,7 +461,7 @@ func TestFormatSchemaFailureDetailLineColumn(t *testing.T) { Path: "/safe-outputs/create-issue", Message: "additional property 'invalid-field' not allowed", } - result := formatSchemaFailureDetail(pathInfo, frontmatterContent, 1) + result := formatSchemaFailureDetail(pathInfo, "", frontmatterContent, 1) if !strings.Contains(result, "line ") || !strings.Contains(result, "column ") { t.Errorf("expected result to contain line/column info, got: %s", result) } diff --git a/pkg/parser/schema_compiler.go b/pkg/parser/schema_compiler.go index 3c9c13bf03..90508ad7de 100644 --- a/pkg/parser/schema_compiler.go +++ b/pkg/parser/schema_compiler.go @@ -246,7 +246,7 @@ func validateWithSchemaAndLocation(frontmatter map[string]any, schemaJSON, conte if len(jsonPaths) > 0 && frontmatterContent != "" { detailLines := make([]string, 0, len(jsonPaths)) for _, pathInfo := range jsonPaths { - detailLines = append(detailLines, formatSchemaFailureDetail(pathInfo, frontmatterContent, frontmatterStart)) + detailLines = append(detailLines, formatSchemaFailureDetail(pathInfo, schemaJSON, frontmatterContent, frontmatterStart)) } // Use the first error path for primary context rendering. @@ -338,7 +338,7 @@ func validateWithSchemaAndLocation(frontmatter map[string]any, schemaJSON, conte return err } -func formatSchemaFailureDetail(pathInfo JSONPathInfo, frontmatterContent string, frontmatterStart int) string { +func formatSchemaFailureDetail(pathInfo JSONPathInfo, schemaJSON, frontmatterContent string, frontmatterStart int) string { path := pathInfo.Path if path == "" { path = "/" @@ -353,6 +353,10 @@ func formatSchemaFailureDetail(pathInfo JSONPathInfo, frontmatterContent string, } message := rewriteAdditionalPropertiesError(cleanOneOfMessage(pathInfo.Message)) + suggestions := generateSchemaBasedSuggestions(schemaJSON, pathInfo.Message, pathInfo.Path, frontmatterContent) + if suggestions != "" { + message = message + ". " + suggestions + } return fmt.Sprintf("at '%s' (line %d, column %d): %s", path, line, column, message) }