Skip to content

Improve compiler error messages for YAML syntax errors and permission scope validation#22581

Merged
pelikhan merged 3 commits intomainfrom
copilot/improve-compiler-error-messages
Mar 24, 2026
Merged

Improve compiler error messages for YAML syntax errors and permission scope validation#22581
pelikhan merged 3 commits intomainfrom
copilot/improve-compiler-error-messages

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 24, 2026

Raw goccy/go-yaml parser messages ("unexpected key name") and permission scope errors lacked actionable guidance. Error quality scored 41/100 for YAML syntax and 68/100 for permission scopes.

YAML error translation (pkg/parser/yaml_error.go)

Added a unified yamlErrorTranslations table (consolidating the previously separate table in pkg/workflow/frontmatter_error.go) and exported TranslateYAMLMessage() as the shared translation function used by both the parser and workflow packages. Translation is scoped to the parser message header only — source context lines shown by yaml.FormatError() are never modified.

Raw parser message Translated
unexpected key name missing ':' after key — YAML mapping entries require 'key: value' format
mapping value is not allowed in this context unexpected ':' — check indentation or if this key belongs in a mapping block
string was used where mapping is expected expected a YAML mapping (key: value pairs) but got a plain string
tab character cannot use as a map key directly tab character in key — YAML requires spaces for indentation, not tabs
non-map value is specified expected a YAML mapping (key: value pairs) — did you forget a colon after the key?
found character that cannot start any token invalid character — check indentation uses spaces, not tabs

The duplicate yamlErrorTranslations / translateYAMLMessage in pkg/workflow/frontmatter_error.go has been removed; that code path now calls parser.TranslateYAMLMessage().

Permission scope error improvements (pkg/parser/schema_errors.go)

Extended appendKnownFieldValidValuesHint for /permissions errors:

  • "Did you mean?" — extracts unknown scope name(s) from the rewritten message and runs FindClosestMatches() against knownFieldScopes
  • Docs link — appends See: https://docs.github.com/... on the same line (avoids breaking multi-error bullet-list formatting)
  • Deterministic parent-path lookup — uses longest-prefix match instead of random map iteration order

Example output for issuess: read:

Unknown property: issuess (Valid permission scopes: ...). Did you mean 'issues'? See: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

… scope validation

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/83c0859d-e78d-49c2-839c-1c54589b16bf
Copilot AI changed the title [WIP] Improve compiler error messages for YAML syntax errors and permission scope validation Improve compiler error messages for YAML syntax errors and permission scope validation Mar 24, 2026
Copilot AI requested a review from pelikhan March 24, 2026 03:55
@pelikhan pelikhan marked this pull request as ready for review March 24, 2026 03:59
Copilot AI review requested due to automatic review settings March 24, 2026 03:59
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Improves end-user diagnostics by translating opaque YAML parser errors into actionable messages and enhancing /permissions schema validation errors with “Did you mean?” suggestions and documentation links.

Changes:

  • Added a YAML parser error translation table and applied it during FormatYAMLError() formatting.
  • Extended schema unknown-property hints for /permissions with closest-match suggestions and a docs URL.
  • Added unit tests covering YAML translation and schema hint behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
pkg/parser/yaml_error.go Adds YAML error translation mapping and applies it in FormatYAMLError().
pkg/parser/yaml_error_test.go Adds tests for translation behavior and for FormatYAMLError() applying translations.
pkg/parser/schema_errors.go Adds scope-aware “Did you mean?” suggestions and docs link for /permissions unknown-property errors.
pkg/parser/schema_errors_test.go Adds tests verifying valid-values hints, suggestions, and docs links.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +58 to +71
func translateYAMLError(formatted string) string {
for _, t := range yamlErrorTranslations {
// All pattern strings are lowercase to match goccy/go-yaml's lowercase error messages.
// Case-insensitive search ensures robustness across library version changes.
lower := strings.ToLower(formatted)
if idx := strings.Index(lower, t.pattern); idx >= 0 {
// Slice using idx from the lowercase string. This is safe because:
// - All patterns are ASCII (single-byte characters).
// - strings.ToLower of ASCII preserves byte positions exactly.
yamlErrorLog.Printf("Translating YAML error pattern %q", t.pattern)
return formatted[:idx] + t.replacement + formatted[idx+len(t.pattern):]
}
}
return formatted
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

translateYAMLError currently scans and rewrites the entire formatted yaml.FormatError() output. Because that output includes the original source lines, this can accidentally replace text inside the user’s YAML content (e.g., if a YAML string contains "unexpected key name"), producing misleading context. Consider restricting translation to just the parser message portion (e.g., only the first line, or the substring after the leading "[line:col] " up to the first newline) and leaving the source context untouched.

Also note: existing tests assert raw goccy messages (e.g. pkg/workflow/compiler_yaml_test.go expects "unexpected key name"); applying this translation will change those outputs and will require updating those expectations.

Suggested change
func translateYAMLError(formatted string) string {
for _, t := range yamlErrorTranslations {
// All pattern strings are lowercase to match goccy/go-yaml's lowercase error messages.
// Case-insensitive search ensures robustness across library version changes.
lower := strings.ToLower(formatted)
if idx := strings.Index(lower, t.pattern); idx >= 0 {
// Slice using idx from the lowercase string. This is safe because:
// - All patterns are ASCII (single-byte characters).
// - strings.ToLower of ASCII preserves byte positions exactly.
yamlErrorLog.Printf("Translating YAML error pattern %q", t.pattern)
return formatted[:idx] + t.replacement + formatted[idx+len(t.pattern):]
}
}
return formatted
//
// IMPORTANT: Only the parser message portion of yaml.FormatError() output is translated.
// The formatted error typically looks like:
// [line:col] original parser message
// > 1 | some: yaml
// ^
// We restrict replacements to the first line (and specifically the substring after "[line:col] ")
// to avoid accidentally rewriting user YAML source shown in the context block.
func translateYAMLError(formatted string) string {
if formatted == "" {
return formatted
}
// Split into first line (header) and the rest (which includes source context).
var header, rest string
if nl := strings.IndexByte(formatted, '\n'); nl >= 0 {
header = formatted[:nl]
rest = formatted[nl:]
} else {
header = formatted
rest = ""
}
// Within the header, try to locate the parser message after the "[line:col] " prefix.
msgStart := strings.Index(header, "] ")
var prefix, msg string
if msgStart >= 0 {
msgStart += len("] ")
prefix = header[:msgStart]
msg = header[msgStart:]
} else {
// Fallback: treat the entire header as the message if no "[line:col] " prefix is found.
prefix = ""
msg = header
}
translated := msg
lower := strings.ToLower(msg)
for _, t := range yamlErrorTranslations {
// All pattern strings are lowercase to match goccy/go-yaml's lowercase error messages.
// Case-insensitive search ensures robustness across library version changes.
if idx := strings.Index(lower, t.pattern); idx >= 0 {
// Slice using idx from the lowercase string. This is safe because:
// - All patterns are ASCII (single-byte characters).
// - strings.ToLower of ASCII preserves byte positions exactly.
yamlErrorLog.Printf("Translating YAML error pattern %q", t.pattern)
translated = msg[:idx] + t.replacement + msg[idx+len(t.pattern):]
break
}
}
// Reassemble: translated header + untouched rest (source context, pointers, etc.).
return prefix + translated + rest

Copilot uses AI. Check for mistakes.

// Append documentation link.
if docsURL != "" {
result = fmt.Sprintf("%s\nSee: %s", result, docsURL)
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

appendKnownFieldValidValuesHint appends the docs URL using a literal newline ("\nSee: …"). This function is used inside formatSchemaFailureDetail(), and those detail strings are later joined into a bullet list ("Multiple schema validation failures:\n- ...\n- ..."). Embedding newlines here will break that formatting (the "See:" line won’t be prefixed with "- ", and the rendered compiler error may become hard to read). Consider keeping the docs URL on the same line (e.g., " See: …" or " (See: …)") or only adding the newline in contexts where multi-line messages are explicitly supported.

Suggested change
result = fmt.Sprintf("%s\nSee: %s", result, docsURL)
result = fmt.Sprintf("%s See: %s", result, docsURL)

Copilot uses AI. Check for mistakes.
Comment on lines +257 to 266
if !hintOK {
for path := range knownFieldValidValues {
if strings.HasPrefix(jsonPath, path+"/") {
hint = h
ok = true
hint = knownFieldValidValues[path]
scopes = knownFieldScopes[path]
docsURL = knownFieldDocs[path]
hintOK = true
break
}
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

The parent-path lookup iterates over knownFieldValidValues (a map) and breaks on the first prefix match. Because Go map iteration order is randomized, if multiple known paths are added in the future with overlapping prefixes, this could select different parents nondeterministically. Consider selecting the longest matching prefix deterministically (e.g., iterate over keys sorted by descending length, or track the best match).

See below for a potential fix:

		// Select the longest matching parent path deterministically.
		bestPath := ""
		bestLen := 0
		for path := range knownFieldValidValues {
			if strings.HasPrefix(jsonPath, path+"/") {
				if l := len(path); l > bestLen {
					bestLen = l
					bestPath = path
				}
			}
		}
		if bestPath != "" {
			hint = knownFieldValidValues[bestPath]
			scopes = knownFieldScopes[bestPath]
			docsURL = knownFieldDocs[bestPath]
			hintOK = true
		}

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +62
for _, t := range yamlErrorTranslations {
// All pattern strings are lowercase to match goccy/go-yaml's lowercase error messages.
// Case-insensitive search ensures robustness across library version changes.
lower := strings.ToLower(formatted)
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

translateYAMLError recalculates strings.ToLower(formatted) on every iteration of yamlErrorTranslations. Since the input doesn’t change inside the loop, compute it once before the loop to avoid repeated allocations/work (especially since FormatYAMLError may be called frequently).

Suggested change
for _, t := range yamlErrorTranslations {
// All pattern strings are lowercase to match goccy/go-yaml's lowercase error messages.
// Case-insensitive search ensures robustness across library version changes.
lower := strings.ToLower(formatted)
// Precompute lowercase version once to avoid repeated allocations/work in the loop.
lower := strings.ToLower(formatted)
for _, t := range yamlErrorTranslations {
// All pattern strings are lowercase to match goccy/go-yaml's lowercase error messages.
// Case-insensitive search ensures robustness across library version changes.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +52
// yamlErrorTranslations maps common goccy/go-yaml internal error messages to
// user-friendly plain-language descriptions with actionable fix guidance.
// Each pattern is matched case-insensitively against the formatted error string.
var yamlErrorTranslations = []struct {
pattern string
replacement string
}{
{
"unexpected key name",
"missing ':' after key — YAML mapping entries require 'key: value' format",
},
{
"mapping value is not allowed in this context",
"unexpected ':' — check indentation or if this key belongs in a mapping block",
},
{
"string was used where mapping is expected",
"expected a YAML mapping (key: value pairs) but got a plain string",
},
{
"tab character cannot use as a map key directly",
"tab character in key — YAML requires spaces for indentation, not tabs",
},
{
"could not find expected ':'",
"missing ':' in key-value pair",
},
{
"did not find expected key",
"incorrect indentation or missing key in mapping",
},
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

This file introduces a second YAML error translation table/function that overlaps with pkg/workflow/frontmatter_error.go (which already has yamlErrorTranslations + translateYAMLMessage for goccy/go-yaml messages). Keeping two separate translation tables risks inconsistent user-facing messages and duplicated maintenance when goccy changes wording. Consider consolidating translations in one place (or sharing a common helper) so both frontmatter and parser formatting paths stay in sync.

Copilot uses AI. Check for mistakes.
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot apply comments

…ocs link, deterministic path lookup

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6fc9b7cb-3dc7-4dda-a277-da4f996f33e4
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 24, 2026

@copilot apply comments

Applied all reviewer comments in commit 1f575a0:

  • Translation scoped to first linetranslateYAMLError now splits the yaml.FormatError() output into header (parser message) and rest (source context), applying TranslateYAMLMessage only to the header. User YAML content in context lines is never modified.
  • Single translation table — removed the duplicate yamlErrorTranslations/translateYAMLMessage from pkg/workflow/frontmatter_error.go; exported parser.TranslateYAMLMessage() as the shared function used by both packages.
  • Docs link on same line — changed \nSee: … to See: … so the URL stays on the same line and doesn't break the "Multiple schema validation failures:" bullet-list format.
  • Deterministic parent-path lookup — replaced the break-on-first-match loop with explicit longest-prefix selection.
  • Updated all affected test expectations in compiler_yaml_test.go, yaml_message_translation_test.go, and the parser test files (including replacing the name-string conditional with a sourceContextCheck struct field).

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw (http block)
  • https://api.github.com/repos/actions/ai-inference/git/ref/tags/v1
    • Triggering command: /usr/bin/gh gh api /repos/actions/ai-inference/git/ref/tags/v1 --jq .object.sha (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v3
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v3 --jq .object.sha P1lAky6x_ 346566/b367/vet.cfg rgo/bin/bash (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v5
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha /tmp/TestHashConsistency_GoAndJavaScript3792503771/001/test-empty-frontmatter.md pTva6LVdgSA1 /usr/bin/git (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha --show-toplevel rf/71zDv45S-drk4CFwZwhM/EdoIStsRK7pHEJjbJNZG /usr/bin/git --get-regexp cfg x_amd64/vet git rev-�� --show-toplevel x_amd64/vet 86_64/node get --local 64/pkg/tool/linu--show-toplevel git (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v6
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha k/gh-aw/gh-aw/pkg/workflow/features_import_test.go k/gh-aw/gh-aw/pkg/workflow/importable_tools_test.go /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha --noprofile -tests /usr/bin/git (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha --show-toplevel 64/pkg/tool/linux_amd64/vet /usr/bin/git ty-test.md (http block)
  • https://api.github.com/repos/actions/github-script/git/ref/tags/v8
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha cgroup2 --noheadings /usr/local/sbin/bash (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha --noprofile -tests 0/x64/bin/node (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha ithub/workflows/blog-auditor.md -tests /usr/lib/sysstat/debian-sa1 (http block)
  • https://api.github.com/repos/actions/setup-go/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-go/git/ref/tags/v4 --jq .object.sha --noprofile -tests /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/actions/setup-node/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v4 --jq .object.sha --noprofile 423247/b001/_testmain.go /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/link (http block)
  • https://api.github.com/repos/actions/upload-artifact/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/upload-artifact/git/ref/tags/v4 --jq .object.sha se 346566/b117/vet.cfg 64/pkg/tool/linux_amd64/cgo (http block)
  • https://api.github.com/repos/github/gh-aw-actions/git/ref/tags/v1.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw-actions/git/ref/tags/v1.0.0 --jq .object.sha se 346566/b121/vet.cfg ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw-actions/git/ref/tags/v1.2.3
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw-actions/git/ref/tags/v1.2.3 --jq .object.sha se 346566/b164/vet.cfg ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.0.0 --jq .object.sha (http block)
  • https://api.github.com/repos/nonexistent/action/git/ref/tags/v999.999.999
    • Triggering command: /usr/bin/gh gh api /repos/nonexistent/action/git/ref/tags/v999.999.999 --jq .object.sha (http block)

If you need me to access, download, or install something from one of these locations, you can either:

@pelikhan pelikhan merged commit e49b9fb into main Mar 24, 2026
77 checks passed
@pelikhan pelikhan deleted the copilot/improve-compiler-error-messages branch March 24, 2026 04:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[syntax-error-quality] Improve compiler error messages for YAML syntax errors and permission scope validation

3 participants