Skip to content

Commit f8befda

Browse files
committed
Allow key-based yaml anchoring
1 parent e389ad2 commit f8befda

2 files changed

Lines changed: 137 additions & 16 deletions

File tree

engine/cld/pipeline/input/yaml.go

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -128,27 +128,41 @@ func GetAllChangesetsInOrder(changesets any) ([]ChangesetItem, error) {
128128
return result, nil
129129
}
130130

131+
// anyToJSONMapKey stringifies a map key for JSON-compatible maps.
132+
func anyToJSONMapKey(v any) string {
133+
switch k := v.(type) {
134+
case string:
135+
return k
136+
case json.Number:
137+
return k.String()
138+
case int:
139+
return strconv.Itoa(k)
140+
case int64:
141+
return strconv.FormatInt(k, 10)
142+
case uint64:
143+
return strconv.FormatUint(k, 10)
144+
case float64:
145+
if k == math.Trunc(k) && (k >= 1e15 || k <= -1e15) {
146+
return strconv.FormatFloat(k, 'f', 0, 64)
147+
}
148+
149+
return strconv.FormatFloat(k, 'f', -1, 64)
150+
default:
151+
return fmt.Sprintf("%v", k)
152+
}
153+
}
154+
155+
func isYAMLMergeKey(node *yaml.Node) bool {
156+
return node != nil && node.Kind == yaml.ScalarNode && (node.Value == "<<" || node.Tag == "!!merge")
157+
}
158+
131159
// ConvertToJSONSafe recursively converts map[interface{}]interface{} to map[string]any.
132160
func ConvertToJSONSafe(data any) (any, error) {
133161
switch v := data.(type) {
134162
case map[interface{}]interface{}:
135163
result := make(map[string]any)
136164
for key, value := range v {
137-
var keyStr string
138-
switch k := key.(type) {
139-
case string:
140-
keyStr = k
141-
case int:
142-
keyStr = strconv.Itoa(k)
143-
case int64:
144-
keyStr = strconv.FormatInt(k, 10)
145-
case uint64:
146-
keyStr = strconv.FormatUint(k, 10)
147-
case float64:
148-
keyStr = strconv.FormatFloat(k, 'f', -1, 64)
149-
default:
150-
keyStr = fmt.Sprintf("%v", k)
151-
}
165+
keyStr := anyToJSONMapKey(key)
152166

153167
convertedValue, err := ConvertToJSONSafe(value)
154168
if err != nil {
@@ -345,7 +359,22 @@ func YamlNodeToAny(node *yaml.Node) any {
345359
for i := 0; i+1 < len(node.Content); i += 2 {
346360
key := node.Content[i]
347361
value := node.Content[i+1]
348-
out[key.Value] = YamlNodeToAny(value)
362+
if isYAMLMergeKey(key) {
363+
mergeMap, ok := YamlNodeToAny(value).(map[string]any)
364+
if !ok {
365+
continue
366+
}
367+
for mk, mv := range mergeMap {
368+
if _, exists := out[mk]; !exists {
369+
out[mk] = mv
370+
}
371+
}
372+
373+
continue
374+
}
375+
376+
keyStr := anyToJSONMapKey(YamlNodeToAny(key))
377+
out[keyStr] = YamlNodeToAny(value)
349378
}
350379

351380
return out

engine/cld/pipeline/input/yaml_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
"github.com/stretchr/testify/require"
10+
"gopkg.in/yaml.v3"
1011

1112
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/domain"
1213
)
@@ -185,6 +186,97 @@ func TestYamlNodeToAny_Nil(t *testing.T) {
185186
require.Nil(t, got)
186187
}
187188

189+
func TestYamlNodeToAny_AliasMapKeys(t *testing.T) {
190+
t.Parallel()
191+
192+
yamlContent := `
193+
sel: &sel_sepolia 16015286601757825753
194+
other: &sel_arb 3478487238524512106
195+
chains:
196+
*sel_sepolia: &cfg
197+
qualifier: UltraFastCurse
198+
timelockMinDelay: 0
199+
*sel_arb: *cfg
200+
`
201+
var root yaml.Node
202+
require.NoError(t, yaml.Unmarshal([]byte(yamlContent), &root))
203+
204+
doc := YamlNodeToAny(&root).(map[string]any)
205+
chains := doc["chains"].(map[string]any)
206+
207+
require.Equal(t, map[string]any{
208+
"16015286601757825753": map[string]any{
209+
"qualifier": "UltraFastCurse",
210+
"timelockMinDelay": json.Number("0"),
211+
},
212+
"3478487238524512106": map[string]any{
213+
"qualifier": "UltraFastCurse",
214+
"timelockMinDelay": json.Number("0"),
215+
},
216+
}, chains)
217+
}
218+
219+
func TestYamlNodeToAny_MergeKeys(t *testing.T) {
220+
t.Parallel()
221+
222+
yamlContent := `
223+
base: &base_cfg
224+
qualifier: UltraFastCurse
225+
timelockMinDelay: 0
226+
chains:
227+
16015286601757825753:
228+
<<: *base_cfg
229+
proposer:
230+
quorum: 1
231+
`
232+
var root yaml.Node
233+
require.NoError(t, yaml.Unmarshal([]byte(yamlContent), &root))
234+
235+
doc := YamlNodeToAny(&root).(map[string]any)
236+
chains := doc["chains"].(map[string]any)
237+
chain := chains["16015286601757825753"].(map[string]any)
238+
239+
require.Equal(t, "UltraFastCurse", chain["qualifier"])
240+
require.Equal(t, json.Number("0"), chain["timelockMinDelay"])
241+
require.Equal(t, map[string]any{"quorum": json.Number("1")}, chain["proposer"])
242+
}
243+
244+
func TestBuildChangesetInputJSON_AliasChainSelectorKeys(t *testing.T) {
245+
t.Parallel()
246+
247+
yamlContent := `
248+
environment: staging_testnet
249+
domain: ccv
250+
changesets:
251+
- deploy_mcms:
252+
chainOverrides:
253+
- &sel_sepolia 16015286601757825753
254+
payload:
255+
adapterVersion: "1.0.0"
256+
chains:
257+
*sel_sepolia:
258+
qualifier: UltraFastCurse
259+
timelockMinDelay: 0
260+
`
261+
dpYAML, err := ParseYAMLBytes([]byte(yamlContent))
262+
require.NoError(t, err)
263+
264+
changesets, err := GetAllChangesetsInOrder(dpYAML.Changesets)
265+
require.NoError(t, err)
266+
require.Len(t, changesets, 1)
267+
268+
inputJSON, err := BuildChangesetInputJSON(changesets[0].Name, changesets[0].Data)
269+
require.NoError(t, err)
270+
271+
var decoded map[string]any
272+
require.NoError(t, json.Unmarshal([]byte(inputJSON), &decoded))
273+
274+
payload := decoded["payload"].(map[string]any)
275+
chains := payload["chains"].(map[string]any)
276+
require.Contains(t, chains, "16015286601757825753")
277+
require.NotContains(t, chains, "sel_sepolia")
278+
}
279+
188280
func TestSetChangesetEnvironmentVariable(t *testing.T) {
189281
t.Parallel()
190282

0 commit comments

Comments
 (0)