From 9d4e6f32d6f626b0ad2ac1506905baa0ad2b6289 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:41:42 +0000 Subject: [PATCH 1/2] Initial plan From 7b9b0833783fc59af573aca6fa217511ca9d1cfa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:00:06 +0000 Subject: [PATCH 2/2] fix: improve schema validation error messages - correct line numbers and cleaner prefixes - Add extractSchemaErrorField() to extract top-level field name from *jsonschema.ValidationError - Use formatCompilerErrorWithPosition with actual field line instead of defaulting to 1:1 - Change 'workflow schema validation failed: GitHub Actions schema validation failed:' to 'invalid workflow: GitHub Actions schema validation failed:' to remove confusing double-prefix - Add TestExtractSchemaErrorField with 4 test cases Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler.go | 14 +++++++- pkg/workflow/schema_validation.go | 15 ++++++++ pkg/workflow/schema_validation_test.go | 48 ++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 48a96b73339..6bcaec47fe1 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -443,8 +443,20 @@ func (c *Compiler) generateAndValidateYAML(workflowData *WorkflowData, markdownP if !c.skipValidation { log.Print("Validating workflow against GitHub Actions schema") if err := c.validateGitHubActionsSchema(yamlContent); err != nil { + // Try to point at the exact line of the failing field in the source markdown. + // extractSchemaErrorField unwraps the error chain to find the top-level field + // name (e.g. "timeout-minutes"), which findFrontmatterFieldLine then locates in + // the source frontmatter so the error is IDE-navigable. + fieldLine := 1 + if fieldName := extractSchemaErrorField(err); fieldName != "" { + frontmatterLines := strings.Split(workflowData.FrontmatterYAML, "\n") + if line := findFrontmatterFieldLine(frontmatterLines, 2, fieldName); line > 0 { + fieldLine = line + } + } // Store error first so we can write invalid YAML before returning - formattedErr := formatCompilerError(markdownPath, "error", fmt.Sprintf("workflow schema validation failed: %v", err), err) + formattedErr := formatCompilerErrorWithPosition(markdownPath, fieldLine, 1, "error", + fmt.Sprintf("invalid workflow: %v", err), err) // Write the invalid YAML to a .invalid.yml file for inspection invalidFile := strings.TrimSuffix(lockFile, ".lock.yml") + ".invalid.yml" if writeErr := os.WriteFile(invalidFile, []byte(yamlContent), 0644); writeErr == nil { diff --git a/pkg/workflow/schema_validation.go b/pkg/workflow/schema_validation.go index 3e73178a605..fb99ea5a5b0 100644 --- a/pkg/workflow/schema_validation.go +++ b/pkg/workflow/schema_validation.go @@ -154,6 +154,21 @@ func extractFieldPath(location []string) string { return location[len(location)-1] // Return the last element as the field name } +// extractSchemaErrorField extracts the top-level field name from a schema validation error. +// It unwraps the error chain to find a *jsonschema.ValidationError and returns the first +// element of InstanceLocation, which is the top-level frontmatter key (e.g. "timeout-minutes"). +// Returns "" if no field name can be extracted. +func extractSchemaErrorField(err error) string { + var ve *jsonschema.ValidationError + if !errors.As(err, &ve) { + return "" + } + if len(ve.InstanceLocation) == 0 { + return "" + } + return ve.InstanceLocation[0] // top-level field name +} + // getFieldExample returns an example for the given field based on the validation error func getFieldExample(fieldPath string, err error) string { // Map of common fields to their examples diff --git a/pkg/workflow/schema_validation_test.go b/pkg/workflow/schema_validation_test.go index 438340806ec..b6ff913d53a 100644 --- a/pkg/workflow/schema_validation_test.go +++ b/pkg/workflow/schema_validation_test.go @@ -3,8 +3,12 @@ package workflow import ( + "errors" + "fmt" "strings" "testing" + + "github.com/santhosh-tekuri/jsonschema/v6" ) func TestExtractFieldPath(t *testing.T) { @@ -236,3 +240,47 @@ jobs: }) } } + +func TestExtractSchemaErrorField(t *testing.T) { + tests := []struct { + name string + err error + expected string + }{ + { + name: "top-level field from ValidationError", + err: fmt.Errorf("wrapped: %w", &jsonschema.ValidationError{ + InstanceLocation: []string{"timeout-minutes"}, + }), + expected: "timeout-minutes", + }, + { + name: "nested field returns top-level", + err: fmt.Errorf("wrapped: %w", &jsonschema.ValidationError{ + InstanceLocation: []string{"jobs", "build", "runs-on"}, + }), + expected: "jobs", + }, + { + name: "non-ValidationError returns empty", + err: errors.New("some other error"), + expected: "", + }, + { + name: "ValidationError with empty location returns empty", + err: fmt.Errorf("wrapped: %w", &jsonschema.ValidationError{ + InstanceLocation: []string{}, + }), + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := extractSchemaErrorField(tt.err) + if result != tt.expected { + t.Errorf("extractSchemaErrorField() = %q, want %q", result, tt.expected) + } + }) + } +}