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
14 changes: 3 additions & 11 deletions pkg/workflow/yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,17 +146,9 @@ func UnquoteYAMLKey(yamlStr string, key string) string {
re = regexp.MustCompile(pattern)
unquoteYAMLKeyCache.Store(key, re)
}
return re.ReplaceAllStringFunc(yamlStr, func(match string) string {
// Find the submatch groups
submatches := re.FindStringSubmatch(match)
if len(submatches) >= 3 {
// submatches[0] is the full match
// submatches[1] is the line start (^ or \n)
// submatches[2] is the whitespace
return submatches[1] + submatches[2] + key + ":"
}
return match
})
// Use ReplaceAllString with capture group references for a single-pass replacement.
// ${1} = line start (^ or \n), ${2} = optional whitespace
return re.ReplaceAllString(yamlStr, "${1}${2}"+key+":")
Comment on lines +149 to +151
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

regexp.Regexp.ReplaceAllString treats $ in the replacement string as a capture-group expansion. Since key is concatenated directly into the replacement, keys containing $ (e.g. user-provided frontmatter keys) would be corrupted by unintended backref expansion. Escape $ in key before building the replacement (e.g. replace $ with $$), or use an API that performs literal insertion for the dynamic portion.

Suggested change
// Use ReplaceAllString with capture group references for a single-pass replacement.
// ${1} = line start (^ or \n), ${2} = optional whitespace
return re.ReplaceAllString(yamlStr, "${1}${2}"+key+":")
// Escape '$' in the key so it is treated literally in the replacement string,
// not as a backreference marker by regexp.ReplaceAllString.
escapedKey := strings.ReplaceAll(key, "$", "$$")
// Use ReplaceAllString with capture group references for a single-pass replacement.
// ${1} = line start (^ or \n), ${2} = optional whitespace
return re.ReplaceAllString(yamlStr, "${1}${2}"+escapedKey+":")

Copilot uses AI. Check for mistakes.
}

// MarshalWithFieldOrder marshals a map to YAML with fields in a specific order.
Expand Down
30 changes: 30 additions & 0 deletions pkg/workflow/yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,36 @@ func TestExtractTopLevelYAMLSectionWithOrdering(t *testing.T) {
}
}

// BenchmarkUnquoteYAMLKey measures single-pass regex replacement performance.
// This benchmark guards against regressions where the implementation calls
// FindStringSubmatch inside a ReplaceAllStringFunc callback (double regex pass).
func BenchmarkUnquoteYAMLKey(b *testing.B) {
// A realistic workflow YAML with the "on" key quoted by the marshaler
yamlStr := `"on":
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
issues:
types:
- opened
workflow_dispatch:
jobs:
agent:
runs-on: ubuntu-latest
steps:
- name: Run
run: echo hello
`
b.ReportAllocs()
for b.Loop() {
_ = UnquoteYAMLKey(yamlStr, "on")
}
}

func TestFormatYAMLValue(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading