Skip to content

Commit 7ecc2db

Browse files
feat(utils): introduce ExecuteOnChainSequenceAndMerge (#1041)
Introduced ExecuteOnChainSequenceAndMerge util which originated from ccip tooling api [here](https://github.com/smartcontractkit/chainlink-ccip/blob/298ed1c38d5cda61a688beb2494a96a5409daf3d/deployment/utils/sequences/sequences.go#L46) This utils allows user to execute multiple sequence and accumulate their results into an object call OnChainOutput ```go import ( "github.com/smartcontractkit/chainlink-deployments-framework/changeset/sequenceutils" ) // Run multiple sequences and accumulate into one OnChainOutput. var agg sequenceutils.OnChainOutput agg, err = sequenceutils.ExecuteOnChainSequenceAndMerge( env.OperationsBundle, deps, deploySeq, deployInput, agg, ) if err != nil { return agg, err } agg, err = sequenceutils.ExecuteOnChainSequenceAndMerge( env.OperationsBundle, deps, configureSeq, configureInput, agg, ) if err != nil { return agg, err // prior merges preserved in agg } ``` JIRA: https://smartcontract-it.atlassian.net/browse/CLD-2464
1 parent 7332038 commit 7ecc2db

3 files changed

Lines changed: 277 additions & 0 deletions

File tree

.changeset/strong-spiders-end.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
feat(utils): new ExecuteOnChainSequenceAndMerge util
6+
7+
Execute sequence and merge output into an aggregate OnChainOutput.

changeset/sequenceutils/merge.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package sequenceutils
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
8+
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
9+
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
10+
)
11+
12+
// ExecuteOnChainSequenceAndMerge executes a sequence and merges the output into the given OnChainOutput.
13+
// On error, the accumulated agg is returned unchanged.
14+
//
15+
// Env metadata is a single record per deployment. If both agg and the sequence output set env metadata
16+
// with different values, merge fails with deployment.ErrInvalidConfig rather than silently overwriting.
17+
// Re-merging the same env (shared pointer or equal Metadata value) is allowed.
18+
func ExecuteOnChainSequenceAndMerge[IN any, DEP any](
19+
b operations.Bundle,
20+
deps DEP,
21+
seq *operations.Sequence[IN, OnChainOutput, DEP],
22+
input IN,
23+
agg OnChainOutput,
24+
) (OnChainOutput, error) {
25+
if seq == nil {
26+
return agg, fmt.Errorf("%w: sequence is required", deployment.ErrInvalidConfig)
27+
}
28+
report, err := operations.ExecuteSequence(b, seq, deps, input)
29+
if err != nil {
30+
return agg, fmt.Errorf("failed to execute %s: %w", seq.ID(), err)
31+
}
32+
if envMetadataConflicts(agg.Metadata.Env, report.Output.Metadata.Env) {
33+
return agg, fmt.Errorf("%w: conflicting env metadata from sequence %s",
34+
deployment.ErrInvalidConfig, seq.ID())
35+
}
36+
agg.BatchOps = append(agg.BatchOps, report.Output.BatchOps...)
37+
agg.Metadata.Addresses = append(agg.Metadata.Addresses, report.Output.Metadata.Addresses...)
38+
agg.Metadata.Contracts = append(agg.Metadata.Contracts, report.Output.Metadata.Contracts...)
39+
agg.Metadata.Chains = append(agg.Metadata.Chains, report.Output.Metadata.Chains...)
40+
if report.Output.Metadata.Env != nil {
41+
agg.Metadata.Env = report.Output.Metadata.Env
42+
}
43+
44+
return agg, nil
45+
}
46+
47+
func envMetadataConflicts(existing, incoming *datastore.EnvMetadata) bool {
48+
if existing == nil || incoming == nil {
49+
return false
50+
}
51+
if existing == incoming {
52+
return false
53+
}
54+
55+
return !reflect.DeepEqual(existing.Metadata, incoming.Metadata)
56+
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package sequenceutils
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/Masterminds/semver/v3"
8+
mcms_types "github.com/smartcontractkit/mcms/types"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
12+
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
13+
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
14+
)
15+
16+
func TestExecuteOnChainSequenceAndMerge_success(t *testing.T) {
17+
t.Parallel()
18+
19+
env := testEnvironment(t)
20+
seq := testSequence(t, func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
21+
return OnChainOutput{
22+
BatchOps: []mcms_types.BatchOperation{sampleBatchOp()},
23+
Metadata: datastore.MetadataBundle{
24+
Addresses: []datastore.AddressRef{{
25+
Address: "0xabc",
26+
ChainSelector: 1,
27+
Type: "Timelock",
28+
Version: semver.MustParse("1.0.0"),
29+
}},
30+
Chains: []datastore.ChainMetadata{{ChainSelector: 1, Metadata: "chain-a"}},
31+
},
32+
}, nil
33+
})
34+
35+
agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq, struct{}{}, OnChainOutput{})
36+
require.NoError(t, err)
37+
require.Len(t, agg.BatchOps, 1)
38+
require.Len(t, agg.Metadata.Addresses, 1)
39+
require.Len(t, agg.Metadata.Chains, 1)
40+
}
41+
42+
func TestExecuteOnChainSequenceAndMerge_preservesAggOnExecuteFailure(t *testing.T) {
43+
t.Parallel()
44+
45+
env := testEnvironment(t)
46+
seqErr := errors.New("sequence failed")
47+
48+
okSeq := testSequence(t, func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
49+
return OnChainOutput{
50+
Metadata: datastore.MetadataBundle{
51+
Addresses: []datastore.AddressRef{{
52+
Address: "0xabc",
53+
ChainSelector: 1,
54+
Type: "Timelock",
55+
Version: semver.MustParse("1.0.0"),
56+
}},
57+
},
58+
}, nil
59+
})
60+
failSeq := operations.NewSequence(
61+
"test-seq-fail",
62+
semver.MustParse("1.0.0"),
63+
"test sequence fail",
64+
func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
65+
return OnChainOutput{}, seqErr
66+
},
67+
)
68+
69+
agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, okSeq, struct{}{}, OnChainOutput{})
70+
require.NoError(t, err)
71+
require.Len(t, agg.Metadata.Addresses, 1)
72+
73+
agg, err = ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, failSeq, struct{}{}, agg)
74+
require.Error(t, err)
75+
require.ErrorContains(t, err, seqErr.Error())
76+
require.Len(t, agg.Metadata.Addresses, 1)
77+
}
78+
79+
func TestExecuteOnChainSequenceAndMerge_appendsChainsWithoutDeduping(t *testing.T) {
80+
t.Parallel()
81+
82+
env := testEnvironment(t)
83+
seq := testSequence(t, func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
84+
return OnChainOutput{
85+
Metadata: datastore.MetadataBundle{
86+
Chains: []datastore.ChainMetadata{{ChainSelector: 1, Metadata: "a"}},
87+
},
88+
}, nil
89+
})
90+
91+
agg := OnChainOutput{}
92+
agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq, struct{}{}, agg)
93+
require.NoError(t, err)
94+
agg, err = ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq, struct{}{}, agg)
95+
require.NoError(t, err)
96+
require.Len(t, agg.Metadata.Chains, 2)
97+
require.Equal(t, uint64(1), agg.Metadata.Chains[0].ChainSelector)
98+
require.Equal(t, uint64(1), agg.Metadata.Chains[1].ChainSelector)
99+
}
100+
101+
func TestExecuteOnChainSequenceAndMerge_nilSequence(t *testing.T) {
102+
t.Parallel()
103+
104+
env := testEnvironment(t)
105+
agg := OnChainOutput{Metadata: datastore.MetadataBundle{
106+
Addresses: []datastore.AddressRef{{Address: "0xabc", ChainSelector: 1, Type: "Timelock", Version: semver.MustParse("1.0.0")}},
107+
}}
108+
109+
out, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, nil, struct{}{}, agg)
110+
require.Error(t, err)
111+
require.ErrorIs(t, err, deployment.ErrInvalidConfig)
112+
require.ErrorContains(t, err, "sequence is required")
113+
require.Equal(t, agg, out)
114+
}
115+
116+
func TestExecuteOnChainSequenceAndMerge_sameEnvPointer(t *testing.T) {
117+
t.Parallel()
118+
119+
env := testEnvironment(t)
120+
envMeta := &datastore.EnvMetadata{Metadata: "shared"}
121+
seq1 := operations.NewSequence(
122+
"test-seq-env-shared-1",
123+
semver.MustParse("1.0.0"),
124+
"test sequence shared env 1",
125+
func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
126+
return OnChainOutput{Metadata: datastore.MetadataBundle{Env: envMeta}}, nil
127+
},
128+
)
129+
seq2 := operations.NewSequence(
130+
"test-seq-env-shared-2",
131+
semver.MustParse("1.0.0"),
132+
"test sequence shared env 2",
133+
func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
134+
return OnChainOutput{Metadata: datastore.MetadataBundle{Env: envMeta}}, nil
135+
},
136+
)
137+
138+
agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq1, struct{}{}, OnChainOutput{})
139+
require.NoError(t, err)
140+
require.Same(t, envMeta, agg.Metadata.Env)
141+
142+
agg, err = ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq2, struct{}{}, agg)
143+
require.NoError(t, err)
144+
require.Same(t, envMeta, agg.Metadata.Env)
145+
}
146+
147+
func TestExecuteOnChainSequenceAndMerge_equivalentEnvValues(t *testing.T) {
148+
t.Parallel()
149+
150+
env := testEnvironment(t)
151+
seq1 := operations.NewSequence(
152+
"test-seq-env-equiv-1",
153+
semver.MustParse("1.0.0"),
154+
"test sequence equivalent env 1",
155+
func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
156+
return OnChainOutput{Metadata: datastore.MetadataBundle{
157+
Env: &datastore.EnvMetadata{Metadata: "shared"},
158+
}}, nil
159+
},
160+
)
161+
seq2 := operations.NewSequence(
162+
"test-seq-env-equiv-2",
163+
semver.MustParse("1.0.0"),
164+
"test sequence equivalent env 2",
165+
func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
166+
return OnChainOutput{Metadata: datastore.MetadataBundle{
167+
Env: &datastore.EnvMetadata{Metadata: "shared"},
168+
}}, nil
169+
},
170+
)
171+
172+
agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq1, struct{}{}, OnChainOutput{})
173+
require.NoError(t, err)
174+
require.Equal(t, "shared", agg.Metadata.Env.Metadata)
175+
176+
agg, err = ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq2, struct{}{}, agg)
177+
require.NoError(t, err)
178+
require.Equal(t, "shared", agg.Metadata.Env.Metadata)
179+
}
180+
181+
func TestExecuteOnChainSequenceAndMerge_envConflict(t *testing.T) {
182+
t.Parallel()
183+
184+
env := testEnvironment(t)
185+
envMeta := &datastore.EnvMetadata{Metadata: "staging"}
186+
seq := operations.NewSequence(
187+
"test-seq-env-conflict",
188+
semver.MustParse("1.0.0"),
189+
"test sequence env conflict",
190+
func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
191+
return OnChainOutput{
192+
BatchOps: []mcms_types.BatchOperation{sampleBatchOp()},
193+
Metadata: datastore.MetadataBundle{
194+
Env: envMeta,
195+
Addresses: []datastore.AddressRef{{
196+
Address: "0xdef",
197+
ChainSelector: 2,
198+
Type: "Timelock",
199+
Version: semver.MustParse("1.0.0"),
200+
}},
201+
},
202+
}, nil
203+
},
204+
)
205+
206+
agg := OnChainOutput{Metadata: datastore.MetadataBundle{Env: &datastore.EnvMetadata{Metadata: "prod"}}}
207+
agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq, struct{}{}, agg)
208+
require.Error(t, err)
209+
require.ErrorIs(t, err, deployment.ErrInvalidConfig)
210+
require.ErrorContains(t, err, "conflicting env metadata")
211+
require.Equal(t, "prod", agg.Metadata.Env.Metadata)
212+
require.Empty(t, agg.BatchOps)
213+
require.Empty(t, agg.Metadata.Addresses)
214+
}

0 commit comments

Comments
 (0)