-
Notifications
You must be signed in to change notification settings - Fork 3
feat(utils): introduce ExecuteOnChainSequenceAndMerge #1041
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "chainlink-deployments-framework": minor | ||
| --- | ||
|
|
||
| feat(utils): new ExecuteOnChainSequenceAndMerge util | ||
|
|
||
| Execute sequence and merge output into an aggregate OnChainOutput. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| package sequenceutils | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "reflect" | ||
|
|
||
| "github.com/smartcontractkit/chainlink-deployments-framework/datastore" | ||
| "github.com/smartcontractkit/chainlink-deployments-framework/deployment" | ||
| "github.com/smartcontractkit/chainlink-deployments-framework/operations" | ||
| ) | ||
|
|
||
| // ExecuteOnChainSequenceAndMerge executes a sequence and merges the output into the given OnChainOutput. | ||
| // On error, the accumulated agg is returned unchanged. | ||
| // | ||
| // Env metadata is a single record per deployment. If both agg and the sequence output set env metadata | ||
| // with different values, merge fails with deployment.ErrInvalidConfig rather than silently overwriting. | ||
| // Re-merging the same env (shared pointer or equal Metadata value) is allowed. | ||
| func ExecuteOnChainSequenceAndMerge[IN any, DEP any]( | ||
| b operations.Bundle, | ||
| deps DEP, | ||
| seq *operations.Sequence[IN, OnChainOutput, DEP], | ||
| input IN, | ||
|
Comment on lines
+20
to
+22
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm finding the ordering of arguments here unintiutive (seq, deps, input would be better to me) but I assume that this keeps it backwards compatible. Not a blocker though
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah yes, the sequence generic type ordering has always been like this type Sequence[IN, OUT, DEP any] struct {
def Definition
handler SequenceHandler[IN, OUT, DEP]
} |
||
| agg OnChainOutput, | ||
| ) (OnChainOutput, error) { | ||
| if seq == nil { | ||
| return agg, fmt.Errorf("%w: sequence is required", deployment.ErrInvalidConfig) | ||
| } | ||
|
Copilot marked this conversation as resolved.
|
||
| report, err := operations.ExecuteSequence(b, seq, deps, input) | ||
| if err != nil { | ||
| return agg, fmt.Errorf("failed to execute %s: %w", seq.ID(), err) | ||
| } | ||
|
Copilot marked this conversation as resolved.
|
||
| if envMetadataConflicts(agg.Metadata.Env, report.Output.Metadata.Env) { | ||
| return agg, fmt.Errorf("%w: conflicting env metadata from sequence %s", | ||
| deployment.ErrInvalidConfig, seq.ID()) | ||
| } | ||
| agg.BatchOps = append(agg.BatchOps, report.Output.BatchOps...) | ||
| agg.Metadata.Addresses = append(agg.Metadata.Addresses, report.Output.Metadata.Addresses...) | ||
| agg.Metadata.Contracts = append(agg.Metadata.Contracts, report.Output.Metadata.Contracts...) | ||
| agg.Metadata.Chains = append(agg.Metadata.Chains, report.Output.Metadata.Chains...) | ||
| if report.Output.Metadata.Env != nil { | ||
| agg.Metadata.Env = report.Output.Metadata.Env | ||
| } | ||
|
|
||
| return agg, nil | ||
| } | ||
|
|
||
| func envMetadataConflicts(existing, incoming *datastore.EnvMetadata) bool { | ||
| if existing == nil || incoming == nil { | ||
| return false | ||
| } | ||
| if existing == incoming { | ||
| return false | ||
| } | ||
|
|
||
| return !reflect.DeepEqual(existing.Metadata, incoming.Metadata) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| package sequenceutils | ||
|
|
||
| import ( | ||
| "errors" | ||
| "testing" | ||
|
|
||
| "github.com/Masterminds/semver/v3" | ||
| mcms_types "github.com/smartcontractkit/mcms/types" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/smartcontractkit/chainlink-deployments-framework/datastore" | ||
| "github.com/smartcontractkit/chainlink-deployments-framework/deployment" | ||
| "github.com/smartcontractkit/chainlink-deployments-framework/operations" | ||
| ) | ||
|
|
||
| func TestExecuteOnChainSequenceAndMerge_success(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| env := testEnvironment(t) | ||
| seq := testSequence(t, func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) { | ||
| return OnChainOutput{ | ||
| BatchOps: []mcms_types.BatchOperation{sampleBatchOp()}, | ||
| Metadata: datastore.MetadataBundle{ | ||
| Addresses: []datastore.AddressRef{{ | ||
| Address: "0xabc", | ||
| ChainSelector: 1, | ||
| Type: "Timelock", | ||
| Version: semver.MustParse("1.0.0"), | ||
| }}, | ||
| Chains: []datastore.ChainMetadata{{ChainSelector: 1, Metadata: "chain-a"}}, | ||
| }, | ||
| }, nil | ||
| }) | ||
|
|
||
| agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq, struct{}{}, OnChainOutput{}) | ||
| require.NoError(t, err) | ||
| require.Len(t, agg.BatchOps, 1) | ||
| require.Len(t, agg.Metadata.Addresses, 1) | ||
| require.Len(t, agg.Metadata.Chains, 1) | ||
| } | ||
|
|
||
| func TestExecuteOnChainSequenceAndMerge_preservesAggOnExecuteFailure(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| env := testEnvironment(t) | ||
| seqErr := errors.New("sequence failed") | ||
|
|
||
| okSeq := testSequence(t, func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) { | ||
| return OnChainOutput{ | ||
| Metadata: datastore.MetadataBundle{ | ||
| Addresses: []datastore.AddressRef{{ | ||
| Address: "0xabc", | ||
| ChainSelector: 1, | ||
| Type: "Timelock", | ||
| Version: semver.MustParse("1.0.0"), | ||
| }}, | ||
| }, | ||
| }, nil | ||
| }) | ||
| failSeq := operations.NewSequence( | ||
| "test-seq-fail", | ||
| semver.MustParse("1.0.0"), | ||
| "test sequence fail", | ||
| func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) { | ||
| return OnChainOutput{}, seqErr | ||
| }, | ||
| ) | ||
|
|
||
| agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, okSeq, struct{}{}, OnChainOutput{}) | ||
| require.NoError(t, err) | ||
| require.Len(t, agg.Metadata.Addresses, 1) | ||
|
|
||
| agg, err = ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, failSeq, struct{}{}, agg) | ||
| require.Error(t, err) | ||
| require.ErrorContains(t, err, seqErr.Error()) | ||
| require.Len(t, agg.Metadata.Addresses, 1) | ||
| } | ||
|
|
||
| func TestExecuteOnChainSequenceAndMerge_appendsChainsWithoutDeduping(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| env := testEnvironment(t) | ||
| seq := testSequence(t, func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) { | ||
| return OnChainOutput{ | ||
| Metadata: datastore.MetadataBundle{ | ||
| Chains: []datastore.ChainMetadata{{ChainSelector: 1, Metadata: "a"}}, | ||
| }, | ||
| }, nil | ||
| }) | ||
|
|
||
| agg := OnChainOutput{} | ||
| agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq, struct{}{}, agg) | ||
| require.NoError(t, err) | ||
| agg, err = ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq, struct{}{}, agg) | ||
| require.NoError(t, err) | ||
| require.Len(t, agg.Metadata.Chains, 2) | ||
| require.Equal(t, uint64(1), agg.Metadata.Chains[0].ChainSelector) | ||
| require.Equal(t, uint64(1), agg.Metadata.Chains[1].ChainSelector) | ||
| } | ||
|
|
||
| func TestExecuteOnChainSequenceAndMerge_nilSequence(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| env := testEnvironment(t) | ||
| agg := OnChainOutput{Metadata: datastore.MetadataBundle{ | ||
| Addresses: []datastore.AddressRef{{Address: "0xabc", ChainSelector: 1, Type: "Timelock", Version: semver.MustParse("1.0.0")}}, | ||
| }} | ||
|
|
||
| out, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, nil, struct{}{}, agg) | ||
| require.Error(t, err) | ||
| require.ErrorIs(t, err, deployment.ErrInvalidConfig) | ||
| require.ErrorContains(t, err, "sequence is required") | ||
| require.Equal(t, agg, out) | ||
| } | ||
|
|
||
| func TestExecuteOnChainSequenceAndMerge_sameEnvPointer(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| env := testEnvironment(t) | ||
| envMeta := &datastore.EnvMetadata{Metadata: "shared"} | ||
| seq1 := operations.NewSequence( | ||
| "test-seq-env-shared-1", | ||
| semver.MustParse("1.0.0"), | ||
| "test sequence shared env 1", | ||
| func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) { | ||
| return OnChainOutput{Metadata: datastore.MetadataBundle{Env: envMeta}}, nil | ||
| }, | ||
| ) | ||
| seq2 := operations.NewSequence( | ||
| "test-seq-env-shared-2", | ||
| semver.MustParse("1.0.0"), | ||
| "test sequence shared env 2", | ||
| func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) { | ||
| return OnChainOutput{Metadata: datastore.MetadataBundle{Env: envMeta}}, nil | ||
| }, | ||
| ) | ||
|
|
||
| agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq1, struct{}{}, OnChainOutput{}) | ||
| require.NoError(t, err) | ||
| require.Same(t, envMeta, agg.Metadata.Env) | ||
|
|
||
| agg, err = ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq2, struct{}{}, agg) | ||
| require.NoError(t, err) | ||
| require.Same(t, envMeta, agg.Metadata.Env) | ||
| } | ||
|
|
||
| func TestExecuteOnChainSequenceAndMerge_equivalentEnvValues(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| env := testEnvironment(t) | ||
| seq1 := operations.NewSequence( | ||
| "test-seq-env-equiv-1", | ||
| semver.MustParse("1.0.0"), | ||
| "test sequence equivalent env 1", | ||
| func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) { | ||
| return OnChainOutput{Metadata: datastore.MetadataBundle{ | ||
| Env: &datastore.EnvMetadata{Metadata: "shared"}, | ||
| }}, nil | ||
| }, | ||
| ) | ||
| seq2 := operations.NewSequence( | ||
| "test-seq-env-equiv-2", | ||
| semver.MustParse("1.0.0"), | ||
| "test sequence equivalent env 2", | ||
| func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) { | ||
| return OnChainOutput{Metadata: datastore.MetadataBundle{ | ||
| Env: &datastore.EnvMetadata{Metadata: "shared"}, | ||
| }}, nil | ||
| }, | ||
| ) | ||
|
|
||
| agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq1, struct{}{}, OnChainOutput{}) | ||
| require.NoError(t, err) | ||
| require.Equal(t, "shared", agg.Metadata.Env.Metadata) | ||
|
|
||
| agg, err = ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq2, struct{}{}, agg) | ||
| require.NoError(t, err) | ||
| require.Equal(t, "shared", agg.Metadata.Env.Metadata) | ||
| } | ||
|
|
||
| func TestExecuteOnChainSequenceAndMerge_envConflict(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| env := testEnvironment(t) | ||
| envMeta := &datastore.EnvMetadata{Metadata: "staging"} | ||
| seq := operations.NewSequence( | ||
| "test-seq-env-conflict", | ||
| semver.MustParse("1.0.0"), | ||
| "test sequence env conflict", | ||
| func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) { | ||
| return OnChainOutput{ | ||
| BatchOps: []mcms_types.BatchOperation{sampleBatchOp()}, | ||
| Metadata: datastore.MetadataBundle{ | ||
| Env: envMeta, | ||
| Addresses: []datastore.AddressRef{{ | ||
| Address: "0xdef", | ||
| ChainSelector: 2, | ||
| Type: "Timelock", | ||
| Version: semver.MustParse("1.0.0"), | ||
| }}, | ||
| }, | ||
| }, nil | ||
| }, | ||
| ) | ||
|
|
||
| agg := OnChainOutput{Metadata: datastore.MetadataBundle{Env: &datastore.EnvMetadata{Metadata: "prod"}}} | ||
| agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq, struct{}{}, agg) | ||
| require.Error(t, err) | ||
| require.ErrorIs(t, err, deployment.ErrInvalidConfig) | ||
| require.ErrorContains(t, err, "conflicting env metadata") | ||
| require.Equal(t, "prod", agg.Metadata.Env.Metadata) | ||
| require.Empty(t, agg.BatchOps) | ||
| require.Empty(t, agg.Metadata.Addresses) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.