@@ -82,8 +82,11 @@ func UpdateYAMLFile(cfg VersionFileConfig, currentVersion, newVersion string) er
8282 newValue = newVersionStr
8383 }
8484
85- // Perform surgical replacement - find and replace only the value
86- newData , err := surgicalReplace (data , oldValue , newValue )
85+ // Extract the key name from the path for targeted replacement
86+ key := extractKeyFromPath (cfg .Path )
87+
88+ // Perform surgical replacement - find and replace only the value for this specific key
89+ newData , err := surgicalReplace (data , key , oldValue , newValue )
8790 if err != nil {
8891 return fmt .Errorf ("replacing value at path %s: %w" , cfg .Path , err )
8992 }
@@ -96,84 +99,111 @@ func UpdateYAMLFile(cfg VersionFileConfig, currentVersion, newVersion string) er
9699 return nil
97100}
98101
102+ // extractKeyFromPath extracts the final key name from a dot-notation path.
103+ // Examples:
104+ //
105+ // "version" -> "version"
106+ // "metadata.version" -> "version"
107+ // "spec.containers[0].image" -> "image"
108+ // "operator.image" -> "image"
109+ func extractKeyFromPath (path string ) string {
110+ // Remove any array indices from the path
111+ re := regexp .MustCompile (`\[\d+\]` )
112+ cleanPath := re .ReplaceAllString (path , "" )
113+
114+ // Get the last component after splitting by dots
115+ parts := strings .Split (cleanPath , "." )
116+ if len (parts ) == 0 {
117+ return path
118+ }
119+ return parts [len (parts )- 1 ]
120+ }
121+
99122// replacementRule defines a pattern-replacement pair for surgical YAML value replacement.
100123// Each rule targets a specific quote style or value format in YAML files.
101124type replacementRule struct {
102125 // name describes what this rule handles (for debugging/documentation)
103126 name string
104- // pattern returns the regex pattern to match for the given old value
105- pattern func (oldValue string ) string
106- // replacement returns the replacement string for the given new value
107- replacement func (newValue string ) string
127+ // pattern returns the regex pattern to match for the given key and old value.
128+ // The key is included to ensure we only match the specific YAML key we're updating.
129+ pattern func (key , oldValue string ) string
130+ // replacement returns the replacement string for the given key and new value
131+ replacement func (key , newValue string ) string
108132}
109133
110134// replacementRules defines all the rules for surgical YAML value replacement.
111135// Rules are tried in order; the first matching rule is applied.
136+ // Each pattern includes the key name to ensure we only replace the value for the
137+ // specific YAML key being updated, not other keys with the same value.
112138var replacementRules = []replacementRule {
113139 {
114140 // Handles double-quoted values: key: "value"
115141 name : "double-quoted" ,
116- pattern : func (oldValue string ) string {
117- return fmt .Sprintf (`"(%s)"` , regexp .QuoteMeta (oldValue ))
142+ pattern : func (key , oldValue string ) string {
143+ return fmt .Sprintf (`(%s:\s*) "(%s)"` , regexp . QuoteMeta ( key ) , regexp .QuoteMeta (oldValue ))
118144 },
119- replacement : func (newValue string ) string {
120- return fmt .Sprintf (`"%s"` , newValue )
145+ replacement : func (_ , newValue string ) string {
146+ // Use ${1} syntax to avoid ambiguity when newValue starts with a digit
147+ return fmt .Sprintf (`${1}"%s"` , newValue )
121148 },
122149 },
123150 {
124151 // Handles single-quoted values: key: 'value'
125152 name : "single-quoted" ,
126- pattern : func (oldValue string ) string {
127- return fmt .Sprintf (`'(%s)'` , regexp .QuoteMeta (oldValue ))
153+ pattern : func (key , oldValue string ) string {
154+ return fmt .Sprintf (`(%s:\s*) '(%s)'` , regexp . QuoteMeta ( key ) , regexp .QuoteMeta (oldValue ))
128155 },
129- replacement : func (newValue string ) string {
130- return fmt .Sprintf (`'%s'` , newValue )
156+ replacement : func (_ , newValue string ) string {
157+ return fmt .Sprintf (`${1} '%s'` , newValue )
131158 },
132159 },
133160 {
134161 // Handles unquoted values at end of line: key: value\n
135162 name : "unquoted-eol" ,
136- pattern : func (oldValue string ) string {
137- return fmt .Sprintf (`: (%s)(\s*)$` , regexp .QuoteMeta (oldValue ))
163+ pattern : func (key , oldValue string ) string {
164+ return fmt .Sprintf (`(%s:\s*) (%s)(\s*)$` , regexp . QuoteMeta ( key ) , regexp .QuoteMeta (oldValue ))
138165 },
139- replacement : func (newValue string ) string {
140- return fmt .Sprintf (`: %s$2 ` , newValue )
166+ replacement : func (_ , newValue string ) string {
167+ return fmt .Sprintf (`${1} %s${3} ` , newValue )
141168 },
142169 },
143170 {
144171 // Handles unquoted values followed by inline comment: key: value # comment
145172 name : "unquoted-with-comment" ,
146- pattern : func (oldValue string ) string {
147- return fmt .Sprintf (`: (%s)(\s*#)` , regexp .QuoteMeta (oldValue ))
173+ pattern : func (key , oldValue string ) string {
174+ return fmt .Sprintf (`(%s:\s*) (%s)(\s*#)` , regexp . QuoteMeta ( key ) , regexp .QuoteMeta (oldValue ))
148175 },
149- replacement : func (newValue string ) string {
150- return fmt .Sprintf (`: %s$2 ` , newValue )
176+ replacement : func (_ , newValue string ) string {
177+ return fmt .Sprintf (`${1} %s${3} ` , newValue )
151178 },
152179 },
153180}
154181
155182// surgicalReplace performs a targeted replacement of a YAML value while preserving
156- // the original formatting (quotes, whitespace, etc.)
157- func surgicalReplace (data []byte , oldValue , newValue string ) ([]byte , error ) {
183+ // the original formatting (quotes, whitespace, etc.). The key parameter ensures
184+ // we only replace the value for the specific YAML key being updated.
185+ func surgicalReplace (data []byte , key , oldValue , newValue string ) ([]byte , error ) {
158186 content := string (data )
159187
160188 // Try each replacement rule in order; use the first one that matches
161189 for _ , rule := range replacementRules {
162- pattern := rule .pattern (oldValue )
190+ pattern := rule .pattern (key , oldValue )
163191 re := regexp .MustCompile (`(?m)` + pattern )
164192 if re .MatchString (content ) {
165- result := re .ReplaceAllString (content , rule .replacement (newValue ))
193+ result := re .ReplaceAllString (content , rule .replacement (key , newValue ))
166194 return []byte (result ), nil
167195 }
168196 }
169197
170- // Fallback: simple string replacement if no pattern matched
171- if strings .Contains (content , oldValue ) {
172- result := strings .Replace (content , oldValue , newValue , 1 )
198+ // Fallback: key-aware simple string replacement if no pattern matched
199+ // Look for "key: oldValue" or "key:oldValue" patterns
200+ keyPattern := regexp .MustCompile (fmt .Sprintf (`(%s:\s*)%s` , regexp .QuoteMeta (key ), regexp .QuoteMeta (oldValue )))
201+ if keyPattern .MatchString (content ) {
202+ result := keyPattern .ReplaceAllString (content , fmt .Sprintf (`${1}%s` , newValue ))
173203 return []byte (result ), nil
174204 }
175205
176- return nil , fmt .Errorf ("could not find value %q to replace" , oldValue )
206+ return nil , fmt .Errorf ("could not find value %q for key %q to replace" , oldValue , key )
177207}
178208
179209// findEmbeddedVersion looks for a version pattern in the value and returns it if found.
0 commit comments