From cce70421120581a13783dd0a83c5e56758b666b3 Mon Sep 17 00:00:00 2001 From: Graham Goh Date: Thu, 16 Oct 2025 14:04:57 +1100 Subject: [PATCH] feat(cli): clone addressbook/datastore merge to dp cli In order to remove migration cli command, we need to support database/addressbook merge on the dp cli side since the ci uses the merge command to merge datastore and address book. --- .changeset/orange-planets-sing.md | 5 + .../legacy/cli/commands/durable-pipelines.go | 162 +++++++++++++++++- .../cli/commands/durable-pipelines_test.go | 133 ++++++++++++++ 3 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 .changeset/orange-planets-sing.md diff --git a/.changeset/orange-planets-sing.md b/.changeset/orange-planets-sing.md new file mode 100644 index 00000000..f17c421e --- /dev/null +++ b/.changeset/orange-planets-sing.md @@ -0,0 +1,5 @@ +--- +"chainlink-deployments-framework": minor +--- + +feat(cli): clone addressbook/datastore merge to dp cli diff --git a/engine/cld/legacy/cli/commands/durable-pipelines.go b/engine/cld/legacy/cli/commands/durable-pipelines.go index 35d0698e..8986b1cb 100644 --- a/engine/cld/legacy/cli/commands/durable-pipelines.go +++ b/engine/cld/legacy/cli/commands/durable-pipelines.go @@ -39,11 +39,27 @@ func (c Commands) NewDurablePipelineCmds( Short: "Durable Pipeline commands", } + addressBookCmd := &cobra.Command{ + Use: "address-book", + Short: "Address book operations", + } + addressBookCmd.AddCommand(c.newDurablePipelineAddressBookMerge(domain)) + addressBookCmd.AddCommand(c.newDurablePipelineAddressBookMigrate(domain)) + + datastoreCmd := &cobra.Command{ + Use: "datastore", + Short: "Datastore operations", + } + datastoreCmd.AddCommand(c.newDurablePipelineDataStoreMerge(domain)) + evmCmd.AddCommand( c.newDurablePipelineRun(domain, loadMigration, decodeProposalCtxProvider, loadConfigResolvers), c.newDurablePipelineInputGenerate(domain, loadMigration, loadConfigResolvers), c.newDurablePipelineListBuild(domain, loadMigration, loadConfigResolvers), - c.newDurablePipelineTemplateInput(domain, loadMigration, loadConfigResolvers)) + c.newDurablePipelineTemplateInput(domain, loadMigration, loadConfigResolvers), + addressBookCmd, + datastoreCmd, + ) evmCmd.PersistentFlags().StringP("environment", "e", "", "Deployment environment (required)") _ = evmCmd.MarkPersistentFlagRequired("environment") @@ -677,3 +693,147 @@ func (c Commands) newDurablePipelineTemplateInput( return &cmd } + +var ( + durablePipelineMergeAddressBookLong = ` + Merges the address book artifact of a specific durable pipeline changeset to the main address book within a + given Domain Environment. This is to ensure that the address book is up-to-date with the + latest changeset changes. + ` + + durablePipelineMergeAddressBookExample = ` + # Merge the address book for the 0001_deploy_cap changeset in the staging environment + ccip durable-pipeline address-book merge --environment staging --name 0001_deploy_cap + + # Merge with a specific timestamp + ccip durable-pipeline address-book merge --environment staging --name 0001_deploy_cap --timestamp 1234567890 + ` +) + +// newDurablePipelineAddressBookMerge creates a command to merge the address books for a durable pipeline changeset to +// the main address book within a given domain environment. +func (Commands) newDurablePipelineAddressBookMerge(domain dom.Domain) *cobra.Command { + var ( + changesetName string + timestamp string + ) + + cmd := cobra.Command{ + Use: "merge", + Short: "Merge the address book", + Long: durablePipelineMergeAddressBookLong, + Example: durablePipelineMergeAddressBookExample, + RunE: func(cmd *cobra.Command, args []string) error { + envKey, _ := cmd.Flags().GetString("environment") + envDir := domain.EnvDir(envKey) + + if err := envDir.MergeMigrationAddressBook(changesetName, timestamp); err != nil { + return fmt.Errorf("error during address book merge for %s %s %s: %w", + domain, envKey, changesetName, err, + ) + } + + cmd.Printf("Merged address books for %s %s %s", + domain, envKey, changesetName, + ) + + return nil + }, + } + + cmd.Flags().StringVarP(&changesetName, "name", "n", "", "name (required)") + cmd.Flags().StringVarP(×tamp, "timestamp", "t", "", "Durable Pipeline timestamp (optional)") + + _ = cmd.MarkFlagRequired("name") + + return &cmd +} + +var ( + durablePipelineMigrateAddressBookLong = ` + Converts the address book artifact format to the new datastore schema within a + given Domain Environment. This updates your on-chain address book to the latest storage format. + ` + + durablePipelineMigrateAddressBookExample = ` + # Migrate the address book for the staging domain to the new datastore format + ccip durable-pipeline address-book migrate --environment staging + ` +) + +// newDurablePipelineAddressBookMigrate creates a command to convert the address book +// artifact to the new datastore format within a given domain environment. +func (Commands) newDurablePipelineAddressBookMigrate(domain dom.Domain) *cobra.Command { + cmd := cobra.Command{ + Use: "migrate", + Short: "Migrate address book to the new datastore format", + Long: durablePipelineMigrateAddressBookLong, + Example: durablePipelineMigrateAddressBookExample, + RunE: func(cmd *cobra.Command, args []string) error { + envKey, _ := cmd.Flags().GetString("environment") + envDir := domain.EnvDir(envKey) + + if err := envDir.MigrateAddressBook(); err != nil { + return fmt.Errorf("error during address book conversion for %s %s: %w", + domain, envKey, err, + ) + } + + cmd.Printf("Address book for %s %s successfully migrated to the new datastore format", + domain, envKey, + ) + + return nil + }, + } + + return &cmd +} + +var ( + durablePipelineDataStoreMergeExample = ` + # Merge the data store for the 0001_deploy_cap changeset in the staging domain + ccip durable-pipeline datastore merge --environment staging --name 0001_deploy_cap + + # Merge with a specific timestamp + ccip durable-pipeline datastore merge --environment staging --name 0001_deploy_cap --timestamp 1234567890 + ` +) + +// newDurablePipelineDataStoreMerge creates a command to merge the data store for a durable pipeline changeset +func (Commands) newDurablePipelineDataStoreMerge(domain dom.Domain) *cobra.Command { + var ( + changesetName string + timestamp string + ) + + cmd := cobra.Command{ + Use: "merge", + Short: "Merge data stores", + Long: "Merge the data store for a changeset to the main data store", + Example: durablePipelineDataStoreMergeExample, + RunE: func(cmd *cobra.Command, args []string) error { + envKey, _ := cmd.Flags().GetString("environment") + envDir := domain.EnvDir(envKey) + + if err := envDir.MergeMigrationDataStore(changesetName, timestamp); err != nil { + return fmt.Errorf("error during data store merge for %s %s %s: %w", + domain, envKey, changesetName, err, + ) + } + + cmd.Printf("Merged data stores for %s %s %s", + domain, envKey, changesetName, + ) + + return nil + }, + } + + cmd.Flags().StringVarP(&changesetName, "name", "n", "", "name (required)") + cmd.Flags().StringVarP(×tamp, "timestamp", "t", "", "Durable Pipeline timestamp (optional)") + + _ = cmd.MarkFlagRequired("name") + + return &cmd +} diff --git a/engine/cld/legacy/cli/commands/durable-pipelines_test.go b/engine/cld/legacy/cli/commands/durable-pipelines_test.go index 5b616f89..e4bffdb5 100644 --- a/engine/cld/legacy/cli/commands/durable-pipelines_test.go +++ b/engine/cld/legacy/cli/commands/durable-pipelines_test.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/spf13/pflag" "github.com/stretchr/testify/require" fresolvers "github.com/smartcontractkit/chainlink-deployments-framework/changeset/resolvers" @@ -1821,3 +1822,135 @@ changesets: require.Error(t, err) require.Contains(t, err.Error(), "--changeset-index can only be used with array format YAML files") } + +func TestNewDurablePipelineCmds_Structure(t *testing.T) { + t.Parallel() + c := NewCommands(nil) + var testDomain domain.Domain + root := c.NewDurablePipelineCmds(testDomain, fakeLoadRegistry, fakeDecodeCtx, nil) + + require.Equal(t, "durable-pipeline", root.Use) + + subs := root.Commands() + require.Len(t, subs, 6, "expected 6 subcommands under 'durable-pipeline'") + + uses := make([]string, len(subs)) + for i, sc := range subs { + uses[i] = sc.Use + } + require.ElementsMatch(t, + []string{"run", "input-generate", "list", "template-input", "address-book", "datastore"}, + uses, + ) + + // The "environment" flag is persistent on root + flag := root.PersistentFlags().Lookup("environment") + require.NotNil(t, flag, "persistent flag 'environment' should exist") + + // address-book group + abIdx := indexOf(subs, "address-book") + require.NotEqual(t, -1, abIdx) + abSubs := subs[abIdx].Commands() + abUses := make([]string, len(abSubs)) + for i, sc := range abSubs { + abUses[i] = sc.Use + } + require.ElementsMatch(t, + []string{"merge", "migrate"}, + abUses, + ) + + // datastore group + dsIdx := indexOf(subs, "datastore") + require.NotEqual(t, -1, dsIdx) + dsSubs := subs[dsIdx].Commands() + dsUses := make([]string, len(dsSubs)) + for i, sc := range dsSubs { + dsUses[i] = sc.Use + } + require.ElementsMatch(t, + []string{"merge"}, + dsUses, + ) +} + +func TestDurablePipelineCommandMetadata(t *testing.T) { + t.Parallel() + c := NewCommands(nil) + testDomain := domain.Domain{} + + tests := []struct { + name string + cmdKey string + wantUse string + wantShort string + wantLongPrefix string + wantExampleContains string + wantFlags []string + }{ + { + name: "address-book merge", + cmdKey: "address-book merge", + wantUse: "merge", + wantShort: "Merge the address book", + wantLongPrefix: "Merges the address book artifact", + wantExampleContains: "address-book merge --environment staging --name", + wantFlags: []string{ + "name", "timestamp", + }, + }, + { + name: "address-book migrate", + cmdKey: "address-book migrate", + wantUse: "migrate", + wantShort: "Migrate address book to the new datastore format", + wantLongPrefix: "Converts the address book artifact format", + wantExampleContains: "address-book migrate --environment staging", + wantFlags: []string{}, + }, + { + name: "datastore merge", + cmdKey: "datastore merge", + wantUse: "merge", + wantShort: "Merge data stores", + wantLongPrefix: "Merge the data store for a changeset", + wantExampleContains: "datastore merge --environment staging --name", + wantFlags: []string{ + "name", "timestamp", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Give each subtest its own fresh command tree + root := c.NewDurablePipelineCmds(testDomain, fakeLoadRegistry, fakeDecodeCtx, nil) + + t.Parallel() + + parts := strings.Split(tc.cmdKey, " ") + cmd, _, err := root.Find(parts) + require.NoError(t, err) + require.NotNil(t, cmd, "command not found: %s", tc.cmdKey) + + require.Equal(t, tc.wantUse, cmd.Use) + require.Contains(t, cmd.Short, tc.wantShort) + require.Contains(t, cmd.Long, tc.wantLongPrefix) + require.Contains(t, cmd.Example, tc.wantExampleContains) + + for _, flagName := range tc.wantFlags { + var flag *pflag.Flag + if flagName == "environment" { + // persistent flag lives on root + flag = root.PersistentFlags().Lookup("environment") + } else { + flag = cmd.Flags().Lookup(flagName) + if flag == nil { + flag = cmd.PersistentFlags().Lookup(flagName) + } + } + require.NotNil(t, flag, "flag %q not found on %s", flagName, tc.name) + } + }) + } +}