From 853fd4abe46e54ce66100b3ab27e358e84710287 Mon Sep 17 00:00:00 2001 From: Jakub Nowak Date: Fri, 17 Apr 2026 13:49:20 +0200 Subject: [PATCH 01/10] initial impl --- cmd/workflow/pause/pause.go | 194 +++-------------- cmd/workflow/pause/pause_test.go | 6 - cmd/workflow/pause/registry_pause_strategy.go | 18 ++ .../pause/registry_pause_strategy_onchain.go | 196 ++++++++++++++++++ .../pause/registry_pause_strategy_private.go | 64 ++++++ .../workflow_private_registry.go | 176 ++++++++++++++++ test/multi_command_test.go | 1 + 7 files changed, 479 insertions(+), 176 deletions(-) create mode 100644 cmd/workflow/pause/registry_pause_strategy.go create mode 100644 cmd/workflow/pause/registry_pause_strategy_onchain.go create mode 100644 cmd/workflow/pause/registry_pause_strategy_private.go diff --git a/cmd/workflow/pause/pause.go b/cmd/workflow/pause/pause.go index 4f7ed8de..1180910d 100644 --- a/cmd/workflow/pause/pause.go +++ b/cmd/workflow/pause/pause.go @@ -1,25 +1,16 @@ package pause import ( - "encoding/hex" "fmt" - "math/big" - "sync" - "time" - "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" "github.com/spf13/cobra" "github.com/spf13/viper" - workflow_registry_v2_wrapper "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/workflow_registry_wrapper_v2" - "github.com/smartcontractkit/cre-cli/cmd/client" - cmdCommon "github.com/smartcontractkit/cre-cli/cmd/common" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/settings" - "github.com/smartcontractkit/cre-cli/internal/types" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" ) @@ -29,10 +20,8 @@ const ( ) type Inputs struct { - WorkflowName string `validate:"workflow_name"` - WorkflowOwner string `validate:"workflow_owner"` - WorkflowRegistryContractAddress string `validate:"required"` - WorkflowRegistryContractChainName string `validate:"required"` + WorkflowName string `validate:"workflow_name"` + WorkflowOwner string `validate:"workflow_owner"` } func New(runtimeContext *runtime.Context) *cobra.Command { @@ -69,13 +58,9 @@ type handler struct { settings *settings.Settings environmentSet *environments.EnvironmentSet inputs Inputs - wrc *client.WorkflowRegistryV2Client runtimeContext *runtime.Context validated bool - - wg sync.WaitGroup - wrcErr error } func newHandler(ctx *runtime.Context) *handler { @@ -86,33 +71,20 @@ func newHandler(ctx *runtime.Context) *handler { environmentSet: ctx.EnvironmentSet, runtimeContext: ctx, validated: false, - wg: sync.WaitGroup{}, - wrcErr: nil, } - h.wg.Add(1) - go func() { - defer h.wg.Done() - wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() - if err != nil { - h.wrcErr = fmt.Errorf("failed to create workflow registry client: %w", err) - return - } - h.wrc = wrc - }() return &h } func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { - oc, err := settings.AsOnChain(h.runtimeContext.ResolvedRegistry, "pause") + resolvedWorkflowOwner, err := h.resolveWorkflowOwner(h.runtimeContext.ResolvedRegistry.Type()) if err != nil { - return Inputs{}, err + return Inputs{}, fmt.Errorf("failed to resolve workflow owner: %w", err) } + return Inputs{ - WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, - WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, - WorkflowRegistryContractChainName: oc.ChainName(), - WorkflowRegistryContractAddress: oc.Address(), + WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, + WorkflowOwner: resolvedWorkflowOwner, }, nil } @@ -135,158 +107,40 @@ func (h *handler) Execute() error { return fmt.Errorf("handler inputs not validated") } - workflowName := h.inputs.WorkflowName - workflowOwner := common.HexToAddress(h.inputs.WorkflowOwner) - h.displayWorkflowDetails() - h.wg.Wait() - if h.wrcErr != nil { - return h.wrcErr - } - - ui.Dim(fmt.Sprintf("Fetching workflows to pause... Name=%s, Owner=%s", workflowName, workflowOwner.Hex())) - - workflows, err := fetchAllWorkflows(h.wrc, workflowOwner, workflowName) + strategy, err := newRegistryPauseStrategy(h.runtimeContext.ResolvedRegistry, h) if err != nil { - return fmt.Errorf("failed to list workflows: %w", err) - } - if len(workflows) == 0 { - return fmt.Errorf("no workflows found for name %q and owner %q", workflowName, workflowOwner.Hex()) + return err } - // Validate precondition: only pause workflows that are currently active - var activeWorkflowIDs [][32]byte - for _, workflow := range workflows { - if workflow.Status == WorkflowStatusActive { - activeWorkflowIDs = append(activeWorkflowIDs, workflow.WorkflowId) - } - } + return strategy.Pause() +} - if len(activeWorkflowIDs) == 0 { - return fmt.Errorf("workflow is already paused, cancelling transaction") +// resolveWorkflowOwner returns the effective owner address for workflow ID computation. +// For private registry deploys, the derived workflow owner from the runtime context is used. +// For onchain deploys, the configured WorkflowOwner address is used directly. +func (h *handler) resolveWorkflowOwner(registryType settings.RegistryType) (string, error) { + if registryType != settings.RegistryTypeOffChain { + return h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, nil } - // Note: The way deploy is set up, there will only ever be one workflow in the command for now - h.runtimeContext.Workflow.ID = hex.EncodeToString(activeWorkflowIDs[0][:]) - - ui.Dim(fmt.Sprintf("Processing batch pause... count=%d", len(activeWorkflowIDs))) - - txOut, err := h.wrc.BatchPauseWorkflows(activeWorkflowIDs) - if err != nil { - return fmt.Errorf("failed to batch pause workflows: %w", err) + owner := h.runtimeContext.DerivedWorkflowOwner + if owner == "" { + return "", fmt.Errorf("derived workflow owner is not available; ensure authentication succeeded") } - oc, _ := h.runtimeContext.ResolvedRegistry.(*settings.OnChainRegistry) - - switch txOut.Type { - case client.Regular: - ui.Success("Transaction confirmed") - ui.URL(fmt.Sprintf("%s/tx/%s", oc.ExplorerURL(), txOut.Hash)) - ui.Success("Workflows paused successfully") - ui.Line() - ui.Bold("Details:") - ui.Dim(fmt.Sprintf(" Contract address: %s", oc.Address())) - ui.Dim(fmt.Sprintf(" Transaction hash: %s", txOut.Hash)) - ui.Dim(fmt.Sprintf(" Workflow Name: %s", workflowName)) - for _, w := range activeWorkflowIDs { - ui.Dim(fmt.Sprintf(" Workflow ID: %s", hex.EncodeToString(w[:]))) - } - - case client.Raw: - ui.Line() - ui.Success("MSIG workflow pause transaction prepared!") - ui.Dim(fmt.Sprintf("To Pause %s", workflowName)) - ui.Line() - ui.Bold("Next steps:") - ui.Line() - ui.Print(" 1. Submit the following transaction on the target chain:") - ui.Dim(fmt.Sprintf(" Chain: %s", h.inputs.WorkflowRegistryContractChainName)) - ui.Dim(fmt.Sprintf(" Contract Address: %s", txOut.RawTx.To)) - ui.Line() - ui.Print(" 2. Use the following transaction data:") - ui.Line() - ui.Code(fmt.Sprintf(" %x", txOut.RawTx.Data)) - ui.Line() - - case client.Changeset: - chainSelector, err := settings.GetChainSelectorByChainName(oc.ChainName()) - if err != nil { - return fmt.Errorf("failed to get chain selector for chain %q: %w", oc.ChainName(), err) - } - mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) - if err != nil { - ui.Warning("MCMS config not found or is incorrect, skipping MCMS config in changeset") - } - cldSettings := h.settings.CLDSettings - changesets := []types.Changeset{ - { - BatchPauseWorkflow: &types.BatchPauseWorkflow{ - Payload: types.UserWorkflowBatchPauseInput{ - WorkflowIDs: h.runtimeContext.Workflow.ID, // Note: The way deploy is set up, there will only ever be one workflow in the command for now - - ChainSelector: chainSelector, - MCMSConfig: mcmsConfig, - WorkflowRegistryQualifier: cldSettings.WorkflowRegistryQualifier, - }, - }, - }, - } - csFile := types.NewChangesetFile(cldSettings.Environment, cldSettings.Domain, cldSettings.MergeProposals, changesets) - - var fileName string - if cldSettings.ChangesetFile != "" { - fileName = cldSettings.ChangesetFile - } else { - fileName = fmt.Sprintf("BatchPauseWorkflow_%s_%s.yaml", workflowName, time.Now().Format("20060102_150405")) - } - - return cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) - - default: - h.log.Warn().Msgf("Unsupported transaction type: %s", txOut.Type) - } - return nil -} - -func fetchAllWorkflows( - wrc interface { - GetWorkflowListByOwnerAndName(owner common.Address, workflowName string, start, limit *big.Int) ([]workflow_registry_v2_wrapper.WorkflowRegistryWorkflowMetadataView, error) - }, - owner common.Address, - name string, -) ([]workflow_registry_v2_wrapper.WorkflowRegistryWorkflowMetadataView, error) { - const pageSize = int64(200) - var ( - start = big.NewInt(0) - limit = big.NewInt(pageSize) - workflows = make([]workflow_registry_v2_wrapper.WorkflowRegistryWorkflowMetadataView, 0, pageSize) - ) - - for { - list, err := wrc.GetWorkflowListByOwnerAndName(owner, name, start, limit) - if err != nil { - return nil, err - } - if len(list) == 0 { - break - } - - workflows = append(workflows, list...) - - start = big.NewInt(start.Int64() + int64(len(list))) - if int64(len(list)) < pageSize { - break - } + if len(owner) >= 2 && owner[:2] != "0x" { + owner = "0x" + owner } - return workflows, nil + return owner, nil } func (h *handler) displayWorkflowDetails() { ui.Line() ui.Title(fmt.Sprintf("Pausing Workflow: %s", h.inputs.WorkflowName)) ui.Dim(fmt.Sprintf("Target: %s", h.settings.User.TargetName)) - ui.Dim(fmt.Sprintf("Owner Address: %s", h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress)) + ui.Dim(fmt.Sprintf("Owner Address: %s", h.inputs.WorkflowOwner)) ui.Line() } diff --git a/cmd/workflow/pause/pause_test.go b/cmd/workflow/pause/pause_test.go index e2022216..3cca5e27 100644 --- a/cmd/workflow/pause/pause_test.go +++ b/cmd/workflow/pause/pause_test.go @@ -18,12 +18,6 @@ func TestWorkflowPauseCommand(t *testing.T) { t.Parallel() validRequired := func(in Inputs) Inputs { - if in.WorkflowRegistryContractAddress == "" { - in.WorkflowRegistryContractAddress = "0x0000000000000000000000000000000000000000" - } - if in.WorkflowRegistryContractChainName == "" { - in.WorkflowRegistryContractChainName = "ethereum-testnet-sepolia" - } return in } diff --git a/cmd/workflow/pause/registry_pause_strategy.go b/cmd/workflow/pause/registry_pause_strategy.go new file mode 100644 index 00000000..cc80c842 --- /dev/null +++ b/cmd/workflow/pause/registry_pause_strategy.go @@ -0,0 +1,18 @@ +package pause + +import ( + "github.com/smartcontractkit/cre-cli/internal/settings" +) + +// registryPauseStrategy encapsulates target-specific pause logic. +type registryPauseStrategy interface { + Pause() error +} + +// newRegistryPauseStrategy returns the appropriate strategy for the given target. +func newRegistryPauseStrategy(resolvedRegistry settings.ResolvedRegistry, h *handler) (registryPauseStrategy, error) { + if resolvedRegistry.Type() == settings.RegistryTypeOffChain { + return newPrivateRegistryPauseStrategy(h), nil + } + return newOnchainRegistryPauseStrategy(h) +} diff --git a/cmd/workflow/pause/registry_pause_strategy_onchain.go b/cmd/workflow/pause/registry_pause_strategy_onchain.go new file mode 100644 index 00000000..c52e793b --- /dev/null +++ b/cmd/workflow/pause/registry_pause_strategy_onchain.go @@ -0,0 +1,196 @@ +package pause + +import ( + "encoding/hex" + "fmt" + "math/big" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + + workflow_registry_v2_wrapper "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/workflow_registry_wrapper_v2" + + "github.com/smartcontractkit/cre-cli/cmd/client" + cmdCommon "github.com/smartcontractkit/cre-cli/cmd/common" + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/types" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +type onchainRegistryPauseStrategy struct { + h *handler + wrc *client.WorkflowRegistryV2Client + onChain *settings.OnChainRegistry + wg sync.WaitGroup + initErr error +} + +func newOnchainRegistryPauseStrategy(h *handler) (*onchainRegistryPauseStrategy, error) { + onChain, err := settings.AsOnChain(h.runtimeContext.ResolvedRegistry, "pause") + if err != nil { + return nil, err + } + + a := &onchainRegistryPauseStrategy{h: h, onChain: onChain} + a.wg.Add(1) + go func() { + defer a.wg.Done() + wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() + if err != nil { + a.initErr = fmt.Errorf("failed to create workflow registry client: %w", err) + return + } + a.wrc = wrc + }() + return a, nil +} + +func (a *onchainRegistryPauseStrategy) Pause() error { + h := a.h + + a.wg.Wait() + if a.initErr != nil { + return a.initErr + } + + workflowName := h.inputs.WorkflowName + workflowOwner := common.HexToAddress(h.inputs.WorkflowOwner) + + ui.Dim(fmt.Sprintf("Fetching workflows to pause... Name=%s, Owner=%s", workflowName, workflowOwner.Hex())) + + workflows, err := fetchAllWorkflows(a.wrc, workflowOwner, workflowName) + if err != nil { + return fmt.Errorf("failed to list workflows: %w", err) + } + if len(workflows) == 0 { + return fmt.Errorf("no workflows found for name %q and owner %q", workflowName, workflowOwner.Hex()) + } + + // Validate precondition: only pause workflows that are currently active + var activeWorkflowIDs [][32]byte + for _, workflow := range workflows { + if workflow.Status == WorkflowStatusActive { + activeWorkflowIDs = append(activeWorkflowIDs, workflow.WorkflowId) + } + } + + if len(activeWorkflowIDs) == 0 { + return fmt.Errorf("workflow is already paused, cancelling transaction") + } + + // Note: The way deploy is set up, there will only ever be one workflow in the command for now + h.runtimeContext.Workflow.ID = hex.EncodeToString(activeWorkflowIDs[0][:]) + + ui.Dim(fmt.Sprintf("Processing batch pause... count=%d", len(activeWorkflowIDs))) + + txOut, err := a.wrc.BatchPauseWorkflows(activeWorkflowIDs) + if err != nil { + return fmt.Errorf("failed to batch pause workflows: %w", err) + } + + oc := a.onChain + + switch txOut.Type { + case client.Regular: + ui.Success("Transaction confirmed") + ui.URL(fmt.Sprintf("%s/tx/%s", oc.ExplorerURL(), txOut.Hash)) + ui.Success("Workflows paused successfully") + ui.Line() + ui.Bold("Details:") + ui.Dim(fmt.Sprintf(" Contract address: %s", oc.Address())) + ui.Dim(fmt.Sprintf(" Transaction hash: %s", txOut.Hash)) + ui.Dim(fmt.Sprintf(" Workflow Name: %s", workflowName)) + for _, w := range activeWorkflowIDs { + ui.Dim(fmt.Sprintf(" Workflow ID: %s", hex.EncodeToString(w[:]))) + } + + case client.Raw: + ui.Line() + ui.Success("MSIG workflow pause transaction prepared!") + ui.Dim(fmt.Sprintf("To Pause %s", workflowName)) + ui.Line() + ui.Bold("Next steps:") + ui.Line() + ui.Print(" 1. Submit the following transaction on the target chain:") + ui.Dim(fmt.Sprintf(" Chain: %s", oc.ChainName())) + ui.Dim(fmt.Sprintf(" Contract Address: %s", txOut.RawTx.To)) + ui.Line() + ui.Print(" 2. Use the following transaction data:") + ui.Line() + ui.Code(fmt.Sprintf(" %x", txOut.RawTx.Data)) + ui.Line() + + case client.Changeset: + chainSelector, err := settings.GetChainSelectorByChainName(oc.ChainName()) + if err != nil { + return fmt.Errorf("failed to get chain selector for chain %q: %w", oc.ChainName(), err) + } + mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) + if err != nil { + ui.Warning("MCMS config not found or is incorrect, skipping MCMS config in changeset") + } + cldSettings := h.settings.CLDSettings + changesets := []types.Changeset{ + { + BatchPauseWorkflow: &types.BatchPauseWorkflow{ + Payload: types.UserWorkflowBatchPauseInput{ + WorkflowIDs: h.runtimeContext.Workflow.ID, // Note: The way deploy is set up, there will only ever be one workflow in the command for now + + ChainSelector: chainSelector, + MCMSConfig: mcmsConfig, + WorkflowRegistryQualifier: cldSettings.WorkflowRegistryQualifier, + }, + }, + }, + } + csFile := types.NewChangesetFile(cldSettings.Environment, cldSettings.Domain, cldSettings.MergeProposals, changesets) + + var fileName string + if cldSettings.ChangesetFile != "" { + fileName = cldSettings.ChangesetFile + } else { + fileName = fmt.Sprintf("BatchPauseWorkflow_%s_%s.yaml", workflowName, time.Now().Format("20060102_150405")) + } + + return cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) + + default: + h.log.Warn().Msgf("Unsupported transaction type: %s", txOut.Type) + } + return nil +} + +func fetchAllWorkflows( + wrc interface { + GetWorkflowListByOwnerAndName(owner common.Address, workflowName string, start, limit *big.Int) ([]workflow_registry_v2_wrapper.WorkflowRegistryWorkflowMetadataView, error) + }, + owner common.Address, + name string, +) ([]workflow_registry_v2_wrapper.WorkflowRegistryWorkflowMetadataView, error) { + const pageSize = int64(200) + var ( + start = big.NewInt(0) + limit = big.NewInt(pageSize) + workflows = make([]workflow_registry_v2_wrapper.WorkflowRegistryWorkflowMetadataView, 0, pageSize) + ) + + for { + list, err := wrc.GetWorkflowListByOwnerAndName(owner, name, start, limit) + if err != nil { + return nil, err + } + if len(list) == 0 { + break + } + + workflows = append(workflows, list...) + + start = big.NewInt(start.Int64() + int64(len(list))) + if int64(len(list)) < pageSize { + break + } + } + + return workflows, nil +} diff --git a/cmd/workflow/pause/registry_pause_strategy_private.go b/cmd/workflow/pause/registry_pause_strategy_private.go new file mode 100644 index 00000000..25ba0602 --- /dev/null +++ b/cmd/workflow/pause/registry_pause_strategy_private.go @@ -0,0 +1,64 @@ +package pause + +import ( + "fmt" + + "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" + "github.com/smartcontractkit/cre-cli/internal/client/privateregistryclient" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +type privateRegistryPauseStrategy struct { + h *handler + prc *privateregistryclient.Client +} + +func newPrivateRegistryPauseStrategy(h *handler) *privateRegistryPauseStrategy { + return &privateRegistryPauseStrategy{h: h} +} + +func (a *privateRegistryPauseStrategy) ensureClient() { + if a.prc == nil { + gql := graphqlclient.New(a.h.runtimeContext.Credentials, a.h.environmentSet, a.h.log) + a.prc = privateregistryclient.New(gql, a.h.log) + } +} + +func (a *privateRegistryPauseStrategy) Pause() error { + a.ensureClient() + + h := a.h + workflowName := h.inputs.WorkflowName + + ui.Dim(fmt.Sprintf("Fetching workflow to pause... Name=%s", workflowName)) + + workflow, err := a.prc.GetWorkflowByName(workflowName) + if err != nil { + return fmt.Errorf("failed to get workflow: %w", err) + } + + if workflow.Status == privateregistryclient.WorkflowStatusPaused { + return fmt.Errorf("workflow is already paused, cancelling transaction") + } + + h.runtimeContext.Workflow.ID = workflow.WorkflowID + + ui.Dim(fmt.Sprintf("Processing pause for workflow ID %s...", workflow.WorkflowID)) + + result, err := a.prc.PauseWorkflowInRegistry(workflow.WorkflowID) + if err != nil { + return fmt.Errorf("failed to pause workflow in private registry: %w", err) + } + + ui.Success("Workflow paused successfully") + ui.Line() + ui.Bold("Details:") + ui.Dim(fmt.Sprintf(" Workflow Name: %s", result.WorkflowName)) + ui.Dim(fmt.Sprintf(" Workflow ID: %s", result.WorkflowID)) + ui.Dim(fmt.Sprintf(" Status: %s", result.Status)) + if result.Owner != "" { + ui.Dim(fmt.Sprintf(" Owner: %s", result.Owner)) + } + + return nil +} diff --git a/test/multi_command_flows/workflow_private_registry.go b/test/multi_command_flows/workflow_private_registry.go index 5b25ae7a..841dc93d 100644 --- a/test/multi_command_flows/workflow_private_registry.go +++ b/test/multi_command_flows/workflow_private_registry.go @@ -277,3 +277,179 @@ func RunWorkflowPrivateRegistryHappyPath(t *testing.T, tc TestConfig) { require.Contains(t, out, "Binary URL:", "expected binary URL in details.\nCLI OUTPUT:\n%s", out) require.Contains(t, out, "Owner: "+privateRegistryOwnerAddress, "expected owner in details.\nCLI OUTPUT:\n%s", out) } + +// workflowPausePrivateRegistry pauses a workflow in the private registry via CLI +// using a mock GraphQL server. +func workflowPausePrivateRegistry(t *testing.T, tc TestConfig) string { + t.Helper() + + var getWorkflowCalled atomic.Bool + var pauseWorkflowCalled atomic.Bool + var srv *httptest.Server + srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasPrefix(r.URL.Path, "/graphql") && r.Method == http.MethodPost: + var req graphQLRequest + _ = json.NewDecoder(r.Body).Decode(&req) + + w.Header().Set("Content-Type", "application/json") + + if strings.Contains(req.Query, "getCreOrganizationInfo") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "GetTenantConfig") || strings.Contains(req.Query, "getTenantConfig") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getTenantConfig": map[string]any{ + "tenantId": "42", + "defaultDonFamily": "test-don", + "vaultGatewayUrl": "https://vault.example.test", + "registries": []map[string]any{ + { + "id": "reg-test", + "label": "reg-test", + "type": "OFF_CHAIN", + "chainSelector": "6433500567565415381", + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "secretsAuthFlows": []string{"BROWSER"}, + }, + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "GetOffchainWorkflowByName") { + getWorkflowCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getOffchainWorkflowByName": map[string]any{ + "workflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "owner": privateRegistryOwnerAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "private-registry-happy-path-workflow", + "binaryUrl": srv.URL + "/get/binary.wasm", + "configUrl": "", + "tag": "private-registry-happy-path-workflow", + "attributes": "", + "donFamily": "test-don", + "organizationId": "test-org-id", + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "PauseOffchainWorkflow") { + pauseWorkflowCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "pauseOffchainWorkflow": map[string]any{ + "workflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "owner": privateRegistryOwnerAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_PAUSED", + "workflowName": "private-registry-happy-path-workflow", + "binaryUrl": srv.URL + "/get/binary.wasm", + "configUrl": "", + "tag": "private-registry-happy-path-workflow", + "attributes": "", + "donFamily": "test-don", + "organizationId": "test-org-id", + }, + }, + }, + }) + return + } + + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "errors": []map[string]string{{"message": "Unsupported GraphQL query"}}, + }) + return + + default: + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("not found")) + return + } + })) + defer srv.Close() + + t.Setenv(environments.EnvVarGraphQLURL, srv.URL+"/graphql") + + args := []string{ + "workflow", "pause", + "blank_workflow", + tc.GetCliEnvFlag(), + tc.GetProjectRootFlag(), + "--" + settings.Flags.SkipConfirmation.Name, + } + + cmd := exec.Command(CLIPath, args...) + testHome := createTestBearerCredentialsHome(t) + + realHome, err := os.UserHomeDir() + require.NoError(t, err, "failed to get real home dir") + + childEnv := make([]string, 0, len(os.Environ())+3) + hasGOPATH := false + for _, entry := range os.Environ() { + if strings.HasPrefix(entry, "HOME=") || strings.HasPrefix(entry, "USERPROFILE=") { + continue + } + if strings.HasPrefix(entry, "GOPATH=") { + hasGOPATH = true + } + childEnv = append(childEnv, entry) + } + childEnv = append(childEnv, "HOME="+testHome, "USERPROFILE="+testHome) + if !hasGOPATH { + childEnv = append(childEnv, "GOPATH="+filepath.Join(realHome, "go")) + } + cmd.Env = childEnv + + var stdout, stderr bytes.Buffer + cmd.Stdout, cmd.Stderr = &stdout, &stderr + + require.NoError( + t, + cmd.Run(), + "cre workflow pause failed:\nSTDOUT:\n%s\nSTDERR:\n%s", + stdout.String(), + stderr.String(), + ) + require.True(t, getWorkflowCalled.Load(), "expected GetOffchainWorkflowByName to be called") + require.True(t, pauseWorkflowCalled.Load(), "expected PauseOffchainWorkflow to be called") + + return StripANSI(stdout.String() + stderr.String()) +} + +// RunWorkflowPausePrivateRegistryHappyPath runs the workflow pause happy path for private registry. +func RunWorkflowPausePrivateRegistryHappyPath(t *testing.T, tc TestConfig) { + t.Helper() + + out := workflowPausePrivateRegistry(t, tc) + require.Contains(t, out, "Workflow paused successfully", "expected private registry pause success.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Details:", "expected details block.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Workflow Name: private-registry-happy-path-workflow", "expected workflow name in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Workflow ID:", "expected workflow ID in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Status: WORKFLOW_STATUS_PAUSED", "expected paused status in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Owner: "+privateRegistryOwnerAddress, "expected owner in details.\nCLI OUTPUT:\n%s", out) +} + diff --git a/test/multi_command_test.go b/test/multi_command_test.go index 062fdea0..8d0a034b 100644 --- a/test/multi_command_test.go +++ b/test/multi_command_test.go @@ -142,6 +142,7 @@ func TestMultiCommandHappyPaths(t *testing.T) { t.Cleanup(tc.Cleanup(t)) multi_command_flows.RunWorkflowPrivateRegistryHappyPath(t, tc) + multi_command_flows.RunWorkflowPausePrivateRegistryHappyPath(t, tc) }) // Run Account Happy Path: Link -> List -> Unlink -> List (verify unlinked) From 2270fd3267ee286276d8c15855c2e80c1d7b4c14 Mon Sep 17 00:00:00 2001 From: Jakub Nowak Date: Fri, 17 Apr 2026 13:49:38 +0200 Subject: [PATCH 02/10] lint --- test/multi_command_flows/workflow_private_registry.go | 1 - 1 file changed, 1 deletion(-) diff --git a/test/multi_command_flows/workflow_private_registry.go b/test/multi_command_flows/workflow_private_registry.go index 841dc93d..0fd0ed80 100644 --- a/test/multi_command_flows/workflow_private_registry.go +++ b/test/multi_command_flows/workflow_private_registry.go @@ -452,4 +452,3 @@ func RunWorkflowPausePrivateRegistryHappyPath(t *testing.T, tc TestConfig) { require.Contains(t, out, "Status: WORKFLOW_STATUS_PAUSED", "expected paused status in details.\nCLI OUTPUT:\n%s", out) require.Contains(t, out, "Owner: "+privateRegistryOwnerAddress, "expected owner in details.\nCLI OUTPUT:\n%s", out) } - From e7e74513f800eed3c646e161f5432a0bf546d400 Mon Sep 17 00:00:00 2001 From: Jakub Nowak Date: Fri, 17 Apr 2026 15:01:04 +0200 Subject: [PATCH 03/10] implement --- cmd/workflow/activate/activate.go | 165 +++------------- cmd/workflow/activate/activate_test.go | 6 - .../activate/registry_activate_strategy.go | 18 ++ .../registry_activate_strategy_onchain.go | 163 ++++++++++++++++ .../registry_activate_strategy_private.go | 64 ++++++ .../workflow_private_registry.go | 183 ++++++++++++++++++ test/multi_command_test.go | 1 + 7 files changed, 455 insertions(+), 145 deletions(-) create mode 100644 cmd/workflow/activate/registry_activate_strategy.go create mode 100644 cmd/workflow/activate/registry_activate_strategy_onchain.go create mode 100644 cmd/workflow/activate/registry_activate_strategy_private.go diff --git a/cmd/workflow/activate/activate.go b/cmd/workflow/activate/activate.go index c30837be..e5d4e40d 100644 --- a/cmd/workflow/activate/activate.go +++ b/cmd/workflow/activate/activate.go @@ -1,24 +1,16 @@ package activate import ( - "encoding/hex" "fmt" - "math/big" - "sort" - "sync" - "time" - "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/smartcontractkit/cre-cli/cmd/client" - cmdCommon "github.com/smartcontractkit/cre-cli/cmd/common" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/settings" - "github.com/smartcontractkit/cre-cli/internal/types" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" ) @@ -28,11 +20,9 @@ const ( ) type Inputs struct { - WorkflowName string `validate:"workflow_name"` - WorkflowOwner string `validate:"workflow_owner"` - DonFamily string `validate:"required"` - WorkflowRegistryContractAddress string `validate:"required"` - WorkflowRegistryContractChainName string `validate:"required"` + WorkflowName string `validate:"workflow_name"` + WorkflowOwner string `validate:"workflow_owner"` + DonFamily string `validate:"required"` } func New(runtimeContext *runtime.Context) *cobra.Command { @@ -70,13 +60,9 @@ type handler struct { settings *settings.Settings environmentSet *environments.EnvironmentSet inputs Inputs - wrc *client.WorkflowRegistryV2Client runtimeContext *runtime.Context validated bool - - wg sync.WaitGroup - wrcErr error } func newHandler(ctx *runtime.Context) *handler { @@ -87,34 +73,21 @@ func newHandler(ctx *runtime.Context) *handler { environmentSet: ctx.EnvironmentSet, runtimeContext: ctx, validated: false, - wg: sync.WaitGroup{}, - wrcErr: nil, } - h.wg.Add(1) - go func() { - defer h.wg.Done() - wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() - if err != nil { - h.wrcErr = fmt.Errorf("failed to create workflow registry client: %w", err) - return - } - h.wrc = wrc - }() return &h } func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { - oc, err := settings.AsOnChain(h.runtimeContext.ResolvedRegistry, "activate") + resolvedWorkflowOwner, err := h.resolveWorkflowOwner(h.runtimeContext.ResolvedRegistry.Type()) if err != nil { - return Inputs{}, err + return Inputs{}, fmt.Errorf("failed to resolve workflow owner: %w", err) } + return Inputs{ - WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, - WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, - DonFamily: oc.DonFamily(), - WorkflowRegistryContractAddress: oc.Address(), - WorkflowRegistryContractChainName: oc.ChainName(), + WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, + WorkflowOwner: resolvedWorkflowOwner, + DonFamily: h.runtimeContext.ResolvedRegistry.DonFamily(), }, nil } @@ -137,126 +110,40 @@ func (h *handler) Execute() error { return fmt.Errorf("handler inputs not validated") } - workflowName := h.inputs.WorkflowName - workflowOwner := h.inputs.WorkflowOwner - h.displayWorkflowDetails() - h.wg.Wait() - if h.wrcErr != nil { - return h.wrcErr - } - - ownerAddr := common.HexToAddress(workflowOwner) - - const pageLimit = 200 - workflows, err := h.wrc.GetWorkflowListByOwnerAndName(ownerAddr, workflowName, big.NewInt(0), big.NewInt(pageLimit)) + strategy, err := newRegistryActivateStrategy(h.runtimeContext.ResolvedRegistry, h) if err != nil { - return fmt.Errorf("failed to get workflow list: %w", err) - } - if len(workflows) == 0 { - return fmt.Errorf("no workflows found for owner=%s name=%s", workflowOwner, workflowName) + return err } - // Sort by CreatedAt descending - sort.Slice(workflows, func(i, j int) bool { - return workflows[i].CreatedAt > workflows[j].CreatedAt - }) - - latest := workflows[0] - - h.runtimeContext.Workflow.ID = hex.EncodeToString(latest.WorkflowId[:]) + return strategy.Activate() +} - // Validate precondition: workflow must be in paused state - if latest.Status != WorkflowStatusPaused { - return fmt.Errorf("workflow is already active, cancelling transaction") +// resolveWorkflowOwner returns the effective owner address for workflow ID computation. +// For private registry deploys, the derived workflow owner from the runtime context is used. +// For onchain deploys, the configured WorkflowOwner address is used directly. +func (h *handler) resolveWorkflowOwner(registryType settings.RegistryType) (string, error) { + if registryType != settings.RegistryTypeOffChain { + return h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, nil } - if err := h.wrc.CheckUserDonLimit(ownerAddr, h.inputs.DonFamily, 1); err != nil { - return err + owner := h.runtimeContext.DerivedWorkflowOwner + if owner == "" { + return "", fmt.Errorf("derived workflow owner is not available; ensure authentication succeeded") } - ui.Dim(fmt.Sprintf("Activating workflow: Name=%s, Owner=%s, WorkflowID=%s", workflowName, workflowOwner, hex.EncodeToString(latest.WorkflowId[:]))) - - txOut, err := h.wrc.ActivateWorkflow(latest.WorkflowId, h.inputs.DonFamily) - if err != nil { - return fmt.Errorf("failed to activate workflow: %w", err) + if len(owner) >= 2 && owner[:2] != "0x" { + owner = "0x" + owner } - oc, _ := h.runtimeContext.ResolvedRegistry.(*settings.OnChainRegistry) - - switch txOut.Type { - case client.Regular: - ui.Success(fmt.Sprintf("Transaction confirmed: %s", txOut.Hash)) - ui.URL(fmt.Sprintf("%s/tx/%s", oc.ExplorerURL(), txOut.Hash)) - ui.Line() - ui.Success("Workflow activated successfully") - ui.Dim(fmt.Sprintf(" Contract address: %s", oc.Address())) - ui.Dim(fmt.Sprintf(" Transaction hash: %s", txOut.Hash)) - ui.Dim(fmt.Sprintf(" Workflow Name: %s", workflowName)) - ui.Dim(fmt.Sprintf(" Workflow ID: %s", hex.EncodeToString(latest.WorkflowId[:]))) - - case client.Raw: - ui.Line() - ui.Success("MSIG workflow activation transaction prepared!") - ui.Dim(fmt.Sprintf("To Activate %s with workflowID: %s", workflowName, hex.EncodeToString(latest.WorkflowId[:]))) - ui.Line() - ui.Bold("Next steps:") - ui.Line() - ui.Print(" 1. Submit the following transaction on the target chain:") - ui.Dim(fmt.Sprintf(" Chain: %s", h.inputs.WorkflowRegistryContractChainName)) - ui.Dim(fmt.Sprintf(" Contract Address: %s", txOut.RawTx.To)) - ui.Line() - ui.Print(" 2. Use the following transaction data:") - ui.Line() - ui.Code(fmt.Sprintf(" %x", txOut.RawTx.Data)) - ui.Line() - - case client.Changeset: - chainSelector, err := settings.GetChainSelectorByChainName(oc.ChainName()) - if err != nil { - return fmt.Errorf("failed to get chain selector for chain %q: %w", oc.ChainName(), err) - } - mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) - if err != nil { - ui.Warning("MCMS config not found or is incorrect, skipping MCMS config in changeset") - } - cldSettings := h.settings.CLDSettings - changesets := []types.Changeset{ - { - ActivateWorkflow: &types.ActivateWorkflow{ - Payload: types.UserWorkflowActivateInput{ - WorkflowID: h.runtimeContext.Workflow.ID, - DonFamily: h.inputs.DonFamily, - - ChainSelector: chainSelector, - MCMSConfig: mcmsConfig, - WorkflowRegistryQualifier: cldSettings.WorkflowRegistryQualifier, - }, - }, - }, - } - csFile := types.NewChangesetFile(cldSettings.Environment, cldSettings.Domain, cldSettings.MergeProposals, changesets) - - var fileName string - if cldSettings.ChangesetFile != "" { - fileName = cldSettings.ChangesetFile - } else { - fileName = fmt.Sprintf("ActivateWorkflow_%s_%s.yaml", workflowName, time.Now().Format("20060102_150405")) - } - - return cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) - - default: - h.log.Warn().Msgf("Unsupported transaction type: %s", txOut.Type) - } - return nil + return owner, nil } func (h *handler) displayWorkflowDetails() { ui.Line() ui.Title(fmt.Sprintf("Activating Workflow: %s", h.inputs.WorkflowName)) ui.Dim(fmt.Sprintf("Target: %s", h.settings.User.TargetName)) - ui.Dim(fmt.Sprintf("Owner Address: %s", h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress)) + ui.Dim(fmt.Sprintf("Owner Address: %s", h.inputs.WorkflowOwner)) ui.Line() } diff --git a/cmd/workflow/activate/activate_test.go b/cmd/workflow/activate/activate_test.go index 07f32e7d..2b06bf00 100644 --- a/cmd/workflow/activate/activate_test.go +++ b/cmd/workflow/activate/activate_test.go @@ -18,12 +18,6 @@ func TestWorkflowActivateCommand(t *testing.T) { t.Parallel() fillRequired := func(in Inputs) Inputs { - if in.WorkflowRegistryContractAddress == "" { - in.WorkflowRegistryContractAddress = "0x0000000000000000000000000000000000000000" - } - if in.WorkflowRegistryContractChainName == "" { - in.WorkflowRegistryContractChainName = "ethereum-testnet-sepolia" - } return in } diff --git a/cmd/workflow/activate/registry_activate_strategy.go b/cmd/workflow/activate/registry_activate_strategy.go new file mode 100644 index 00000000..038241f6 --- /dev/null +++ b/cmd/workflow/activate/registry_activate_strategy.go @@ -0,0 +1,18 @@ +package activate + +import ( + "github.com/smartcontractkit/cre-cli/internal/settings" +) + +// registryActivateStrategy encapsulates target-specific activate logic. +type registryActivateStrategy interface { + Activate() error +} + +// newRegistryActivateStrategy returns the appropriate strategy for the given target. +func newRegistryActivateStrategy(resolvedRegistry settings.ResolvedRegistry, h *handler) (registryActivateStrategy, error) { + if resolvedRegistry.Type() == settings.RegistryTypeOffChain { + return newPrivateRegistryActivateStrategy(h), nil + } + return newOnchainRegistryActivateStrategy(h) +} diff --git a/cmd/workflow/activate/registry_activate_strategy_onchain.go b/cmd/workflow/activate/registry_activate_strategy_onchain.go new file mode 100644 index 00000000..e1acd89b --- /dev/null +++ b/cmd/workflow/activate/registry_activate_strategy_onchain.go @@ -0,0 +1,163 @@ +package activate + +import ( + "encoding/hex" + "fmt" + "math/big" + "sort" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/cre-cli/cmd/client" + cmdCommon "github.com/smartcontractkit/cre-cli/cmd/common" + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/types" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +type onchainRegistryActivateStrategy struct { + h *handler + wrc *client.WorkflowRegistryV2Client + onChain *settings.OnChainRegistry + wg sync.WaitGroup + initErr error +} + +func newOnchainRegistryActivateStrategy(h *handler) (*onchainRegistryActivateStrategy, error) { + onChain, err := settings.AsOnChain(h.runtimeContext.ResolvedRegistry, "activate") + if err != nil { + return nil, err + } + + a := &onchainRegistryActivateStrategy{h: h, onChain: onChain} + a.wg.Add(1) + go func() { + defer a.wg.Done() + wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() + if err != nil { + a.initErr = fmt.Errorf("failed to create workflow registry client: %w", err) + return + } + a.wrc = wrc + }() + return a, nil +} + +func (a *onchainRegistryActivateStrategy) Activate() error { + h := a.h + + a.wg.Wait() + if a.initErr != nil { + return a.initErr + } + + workflowName := h.inputs.WorkflowName + workflowOwner := h.inputs.WorkflowOwner + + ownerAddr := common.HexToAddress(workflowOwner) + + const pageLimit = 200 + workflows, err := a.wrc.GetWorkflowListByOwnerAndName(ownerAddr, workflowName, big.NewInt(0), big.NewInt(pageLimit)) + if err != nil { + return fmt.Errorf("failed to get workflow list: %w", err) + } + if len(workflows) == 0 { + return fmt.Errorf("no workflows found for owner=%s name=%s", workflowOwner, workflowName) + } + + // Sort by CreatedAt descending + sort.Slice(workflows, func(i, j int) bool { + return workflows[i].CreatedAt > workflows[j].CreatedAt + }) + + latest := workflows[0] + + h.runtimeContext.Workflow.ID = hex.EncodeToString(latest.WorkflowId[:]) + + // Validate precondition: workflow must be in paused state + if latest.Status != WorkflowStatusPaused { + return fmt.Errorf("workflow is already active, cancelling transaction") + } + + if err := a.wrc.CheckUserDonLimit(ownerAddr, h.inputs.DonFamily, 1); err != nil { + return err + } + + ui.Dim(fmt.Sprintf("Activating workflow: Name=%s, Owner=%s, WorkflowID=%s", workflowName, workflowOwner, hex.EncodeToString(latest.WorkflowId[:]))) + + txOut, err := a.wrc.ActivateWorkflow(latest.WorkflowId, h.inputs.DonFamily) + if err != nil { + return fmt.Errorf("failed to activate workflow: %w", err) + } + + oc := a.onChain + + switch txOut.Type { + case client.Regular: + ui.Success(fmt.Sprintf("Transaction confirmed: %s", txOut.Hash)) + ui.URL(fmt.Sprintf("%s/tx/%s", oc.ExplorerURL(), txOut.Hash)) + ui.Line() + ui.Success("Workflow activated successfully") + ui.Dim(fmt.Sprintf(" Contract address: %s", oc.Address())) + ui.Dim(fmt.Sprintf(" Transaction hash: %s", txOut.Hash)) + ui.Dim(fmt.Sprintf(" Workflow Name: %s", workflowName)) + ui.Dim(fmt.Sprintf(" Workflow ID: %s", hex.EncodeToString(latest.WorkflowId[:]))) + + case client.Raw: + ui.Line() + ui.Success("MSIG workflow activation transaction prepared!") + ui.Dim(fmt.Sprintf("To Activate %s with workflowID: %s", workflowName, hex.EncodeToString(latest.WorkflowId[:]))) + ui.Line() + ui.Bold("Next steps:") + ui.Line() + ui.Print(" 1. Submit the following transaction on the target chain:") + ui.Dim(fmt.Sprintf(" Chain: %s", oc.ChainName())) + ui.Dim(fmt.Sprintf(" Contract Address: %s", txOut.RawTx.To)) + ui.Line() + ui.Print(" 2. Use the following transaction data:") + ui.Line() + ui.Code(fmt.Sprintf(" %x", txOut.RawTx.Data)) + ui.Line() + + case client.Changeset: + chainSelector, err := settings.GetChainSelectorByChainName(oc.ChainName()) + if err != nil { + return fmt.Errorf("failed to get chain selector for chain %q: %w", oc.ChainName(), err) + } + mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) + if err != nil { + ui.Warning("MCMS config not found or is incorrect, skipping MCMS config in changeset") + } + cldSettings := h.settings.CLDSettings + changesets := []types.Changeset{ + { + ActivateWorkflow: &types.ActivateWorkflow{ + Payload: types.UserWorkflowActivateInput{ + WorkflowID: h.runtimeContext.Workflow.ID, + DonFamily: h.inputs.DonFamily, + + ChainSelector: chainSelector, + MCMSConfig: mcmsConfig, + WorkflowRegistryQualifier: cldSettings.WorkflowRegistryQualifier, + }, + }, + }, + } + csFile := types.NewChangesetFile(cldSettings.Environment, cldSettings.Domain, cldSettings.MergeProposals, changesets) + + var fileName string + if cldSettings.ChangesetFile != "" { + fileName = cldSettings.ChangesetFile + } else { + fileName = fmt.Sprintf("ActivateWorkflow_%s_%s.yaml", workflowName, time.Now().Format("20060102_150405")) + } + + return cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) + + default: + h.log.Warn().Msgf("Unsupported transaction type: %s", txOut.Type) + } + return nil +} diff --git a/cmd/workflow/activate/registry_activate_strategy_private.go b/cmd/workflow/activate/registry_activate_strategy_private.go new file mode 100644 index 00000000..f63079e1 --- /dev/null +++ b/cmd/workflow/activate/registry_activate_strategy_private.go @@ -0,0 +1,64 @@ +package activate + +import ( + "fmt" + + "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" + "github.com/smartcontractkit/cre-cli/internal/client/privateregistryclient" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +type privateRegistryActivateStrategy struct { + h *handler + prc *privateregistryclient.Client +} + +func newPrivateRegistryActivateStrategy(h *handler) *privateRegistryActivateStrategy { + return &privateRegistryActivateStrategy{h: h} +} + +func (a *privateRegistryActivateStrategy) ensureClient() { + if a.prc == nil { + gql := graphqlclient.New(a.h.runtimeContext.Credentials, a.h.environmentSet, a.h.log) + a.prc = privateregistryclient.New(gql, a.h.log) + } +} + +func (a *privateRegistryActivateStrategy) Activate() error { + a.ensureClient() + + h := a.h + workflowName := h.inputs.WorkflowName + + ui.Dim(fmt.Sprintf("Fetching workflow to activate... Name=%s", workflowName)) + + workflow, err := a.prc.GetWorkflowByName(workflowName) + if err != nil { + return fmt.Errorf("failed to get workflow: %w", err) + } + + if workflow.Status == privateregistryclient.WorkflowStatusActive { + return fmt.Errorf("workflow is already active, cancelling transaction") + } + + h.runtimeContext.Workflow.ID = workflow.WorkflowID + + ui.Dim(fmt.Sprintf("Processing activation for workflow ID %s...", workflow.WorkflowID)) + + result, err := a.prc.ActivateWorkflowInRegistry(workflow.WorkflowID) + if err != nil { + return fmt.Errorf("failed to activate workflow in private registry: %w", err) + } + + ui.Success("Workflow activated successfully") + ui.Line() + ui.Bold("Details:") + ui.Dim(fmt.Sprintf(" Workflow Name: %s", result.WorkflowName)) + ui.Dim(fmt.Sprintf(" Workflow ID: %s", result.WorkflowID)) + ui.Dim(fmt.Sprintf(" Status: %s", result.Status)) + if result.Owner != "" { + ui.Dim(fmt.Sprintf(" Owner: %s", result.Owner)) + } + + return nil +} diff --git a/test/multi_command_flows/workflow_private_registry.go b/test/multi_command_flows/workflow_private_registry.go index 0fd0ed80..841fc2ff 100644 --- a/test/multi_command_flows/workflow_private_registry.go +++ b/test/multi_command_flows/workflow_private_registry.go @@ -444,6 +444,14 @@ func workflowPausePrivateRegistry(t *testing.T, tc TestConfig) string { func RunWorkflowPausePrivateRegistryHappyPath(t *testing.T, tc TestConfig) { t.Helper() + projectRoot := strings.TrimPrefix(tc.GetProjectRootFlag(), "--project-root=") + workflowYamlPath := filepath.Join(projectRoot, "blank_workflow", "workflow.yaml") + b, err := os.ReadFile(workflowYamlPath) + require.NoError(t, err) + newYaml := strings.Replace(string(b), "workflow-name:", "deployment-registry: reg-test\n workflow-name:", 1) + err = os.WriteFile(workflowYamlPath, []byte(newYaml), 0644) + require.NoError(t, err) + out := workflowPausePrivateRegistry(t, tc) require.Contains(t, out, "Workflow paused successfully", "expected private registry pause success.\nCLI OUTPUT:\n%s", out) require.Contains(t, out, "Details:", "expected details block.\nCLI OUTPUT:\n%s", out) @@ -452,3 +460,178 @@ func RunWorkflowPausePrivateRegistryHappyPath(t *testing.T, tc TestConfig) { require.Contains(t, out, "Status: WORKFLOW_STATUS_PAUSED", "expected paused status in details.\nCLI OUTPUT:\n%s", out) require.Contains(t, out, "Owner: "+privateRegistryOwnerAddress, "expected owner in details.\nCLI OUTPUT:\n%s", out) } + +// workflowActivatePrivateRegistry activates a workflow in the private registry via CLI +// using a mock GraphQL server. +func workflowActivatePrivateRegistry(t *testing.T, tc TestConfig) string { + t.Helper() + + var getWorkflowCalled atomic.Bool + var activateWorkflowCalled atomic.Bool + var srv *httptest.Server + srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasPrefix(r.URL.Path, "/graphql") && r.Method == http.MethodPost: + var req graphQLRequest + _ = json.NewDecoder(r.Body).Decode(&req) + + w.Header().Set("Content-Type", "application/json") + + if strings.Contains(req.Query, "getCreOrganizationInfo") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "GetTenantConfig") || strings.Contains(req.Query, "getTenantConfig") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getTenantConfig": map[string]any{ + "tenantId": "42", + "defaultDonFamily": "test-don", + "vaultGatewayUrl": "https://vault.example.test", + "registries": []map[string]any{ + { + "id": "reg-test", + "label": "reg-test", + "type": "OFF_CHAIN", + "chainSelector": "6433500567565415381", + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "secretsAuthFlows": []string{"BROWSER"}, + }, + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "GetOffchainWorkflowByName") { + getWorkflowCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getOffchainWorkflowByName": map[string]any{ + "workflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "owner": privateRegistryOwnerAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_PAUSED", + "workflowName": "private-registry-happy-path-workflow", + "binaryUrl": srv.URL + "/get/binary.wasm", + "configUrl": "", + "tag": "private-registry-happy-path-workflow", + "attributes": "", + "donFamily": "test-don", + "organizationId": "test-org-id", + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "ActivateOffchainWorkflow") { + activateWorkflowCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "activateOffchainWorkflow": map[string]any{ + "workflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "owner": privateRegistryOwnerAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "private-registry-happy-path-workflow", + "binaryUrl": srv.URL + "/get/binary.wasm", + "configUrl": "", + "tag": "private-registry-happy-path-workflow", + "attributes": "", + "donFamily": "test-don", + "organizationId": "test-org-id", + }, + }, + }, + }) + return + } + + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "errors": []map[string]string{{"message": "Unsupported GraphQL query"}}, + }) + return + + default: + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("not found")) + return + } + })) + defer srv.Close() + + t.Setenv(environments.EnvVarGraphQLURL, srv.URL+"/graphql") + + args := []string{ + "workflow", "activate", + "blank_workflow", + tc.GetCliEnvFlag(), + tc.GetProjectRootFlag(), + "--" + settings.Flags.SkipConfirmation.Name, + } + + cmd := exec.Command(CLIPath, args...) + testHome := createTestBearerCredentialsHome(t) + + realHome, err := os.UserHomeDir() + require.NoError(t, err, "failed to get real home dir") + + childEnv := make([]string, 0, len(os.Environ())+3) + hasGOPATH := false + for _, entry := range os.Environ() { + if strings.HasPrefix(entry, "HOME=") || strings.HasPrefix(entry, "USERPROFILE=") { + continue + } + if strings.HasPrefix(entry, "GOPATH=") { + hasGOPATH = true + } + childEnv = append(childEnv, entry) + } + childEnv = append(childEnv, "HOME="+testHome, "USERPROFILE="+testHome) + if !hasGOPATH { + childEnv = append(childEnv, "GOPATH="+filepath.Join(realHome, "go")) + } + cmd.Env = childEnv + + var stdout, stderr bytes.Buffer + cmd.Stdout, cmd.Stderr = &stdout, &stderr + + require.NoError( + t, + cmd.Run(), + "cre workflow activate failed:\nSTDOUT:\n%s\nSTDERR:\n%s", + stdout.String(), + stderr.String(), + ) + require.True(t, getWorkflowCalled.Load(), "expected GetOffchainWorkflowByName to be called") + require.True(t, activateWorkflowCalled.Load(), "expected ActivateOffchainWorkflow to be called") + + return StripANSI(stdout.String() + stderr.String()) +} + +// RunWorkflowActivatePrivateRegistryHappyPath runs the workflow activate happy path for private registry. +func RunWorkflowActivatePrivateRegistryHappyPath(t *testing.T, tc TestConfig) { + t.Helper() + + out := workflowActivatePrivateRegistry(t, tc) + require.Contains(t, out, "Workflow activated successfully", "expected private registry activate success.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Details:", "expected details block.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Workflow Name: private-registry-happy-path-workflow", "expected workflow name in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Workflow ID:", "expected workflow ID in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Status: WORKFLOW_STATUS_ACTIVE", "expected active status in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Owner: "+privateRegistryOwnerAddress, "expected owner in details.\nCLI OUTPUT:\n%s", out) +} diff --git a/test/multi_command_test.go b/test/multi_command_test.go index 8d0a034b..26af85a9 100644 --- a/test/multi_command_test.go +++ b/test/multi_command_test.go @@ -143,6 +143,7 @@ func TestMultiCommandHappyPaths(t *testing.T) { multi_command_flows.RunWorkflowPrivateRegistryHappyPath(t, tc) multi_command_flows.RunWorkflowPausePrivateRegistryHappyPath(t, tc) + multi_command_flows.RunWorkflowActivatePrivateRegistryHappyPath(t, tc) }) // Run Account Happy Path: Link -> List -> Unlink -> List (verify unlinked) From fb480823259b1cf3e7323349e184ad2ba3f4c396 Mon Sep 17 00:00:00 2001 From: Jakub Nowak Date: Fri, 17 Apr 2026 15:12:39 +0200 Subject: [PATCH 04/10] linter --- test/multi_command_flows/workflow_private_registry.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/multi_command_flows/workflow_private_registry.go b/test/multi_command_flows/workflow_private_registry.go index 841fc2ff..9435fcf2 100644 --- a/test/multi_command_flows/workflow_private_registry.go +++ b/test/multi_command_flows/workflow_private_registry.go @@ -446,10 +446,10 @@ func RunWorkflowPausePrivateRegistryHappyPath(t *testing.T, tc TestConfig) { projectRoot := strings.TrimPrefix(tc.GetProjectRootFlag(), "--project-root=") workflowYamlPath := filepath.Join(projectRoot, "blank_workflow", "workflow.yaml") - b, err := os.ReadFile(workflowYamlPath) + b, err := os.ReadFile(workflowYamlPath) //nolint:gosec // G304 -- test file require.NoError(t, err) newYaml := strings.Replace(string(b), "workflow-name:", "deployment-registry: reg-test\n workflow-name:", 1) - err = os.WriteFile(workflowYamlPath, []byte(newYaml), 0644) + err = os.WriteFile(workflowYamlPath, []byte(newYaml), 0o600) //nolint:gosec // G703 -- test file require.NoError(t, err) out := workflowPausePrivateRegistry(t, tc) From 245ced3312936d588322429b54127d9b5c484e4c Mon Sep 17 00:00:00 2001 From: Jakub Nowak Date: Fri, 17 Apr 2026 16:10:15 +0200 Subject: [PATCH 05/10] fix e2e test --- test/multi_command_flows/workflow_private_registry.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/multi_command_flows/workflow_private_registry.go b/test/multi_command_flows/workflow_private_registry.go index 9435fcf2..1b800b1a 100644 --- a/test/multi_command_flows/workflow_private_registry.go +++ b/test/multi_command_flows/workflow_private_registry.go @@ -14,6 +14,7 @@ import ( "testing" "time" + "github.com/spf13/viper" "github.com/stretchr/testify/require" "github.com/smartcontractkit/cre-cli/internal/environments" @@ -446,10 +447,14 @@ func RunWorkflowPausePrivateRegistryHappyPath(t *testing.T, tc TestConfig) { projectRoot := strings.TrimPrefix(tc.GetProjectRootFlag(), "--project-root=") workflowYamlPath := filepath.Join(projectRoot, "blank_workflow", "workflow.yaml") - b, err := os.ReadFile(workflowYamlPath) //nolint:gosec // G304 -- test file + + v := viper.New() + v.SetConfigFile(workflowYamlPath) + err := v.ReadInConfig() require.NoError(t, err) - newYaml := strings.Replace(string(b), "workflow-name:", "deployment-registry: reg-test\n workflow-name:", 1) - err = os.WriteFile(workflowYamlPath, []byte(newYaml), 0o600) //nolint:gosec // G703 -- test file + + v.Set("staging-settings.user-workflow.deployment-registry", "reg-test") + err = v.WriteConfig() require.NoError(t, err) out := workflowPausePrivateRegistry(t, tc) From 078f2cd8589e7633bbd4822d5e809b434aeca15e Mon Sep 17 00:00:00 2001 From: Jakub Nowak Date: Fri, 17 Apr 2026 16:18:00 +0200 Subject: [PATCH 06/10] linter --- test/multi_command_flows/workflow_private_registry.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/multi_command_flows/workflow_private_registry.go b/test/multi_command_flows/workflow_private_registry.go index 1b800b1a..4d8a4be4 100644 --- a/test/multi_command_flows/workflow_private_registry.go +++ b/test/multi_command_flows/workflow_private_registry.go @@ -447,12 +447,12 @@ func RunWorkflowPausePrivateRegistryHappyPath(t *testing.T, tc TestConfig) { projectRoot := strings.TrimPrefix(tc.GetProjectRootFlag(), "--project-root=") workflowYamlPath := filepath.Join(projectRoot, "blank_workflow", "workflow.yaml") - + v := viper.New() v.SetConfigFile(workflowYamlPath) err := v.ReadInConfig() require.NoError(t, err) - + v.Set("staging-settings.user-workflow.deployment-registry", "reg-test") err = v.WriteConfig() require.NoError(t, err) From 4013122e4d860743e018ad698508576d393ecfbb Mon Sep 17 00:00:00 2001 From: Jakub Nowak Date: Fri, 17 Apr 2026 15:39:05 +0200 Subject: [PATCH 07/10] initial implementation --- cmd/workflow/delete/delete.go | 184 +++--------------- cmd/workflow/delete/delete_test.go | 7 - .../delete/registry_delete_strategy.go | 18 ++ .../registry_delete_strategy_onchain.go | 173 ++++++++++++++++ .../registry_delete_strategy_private.go | 76 ++++++++ .../workflow_private_registry.go | 159 +++++++++++++++ test/multi_command_test.go | 1 + 7 files changed, 456 insertions(+), 162 deletions(-) create mode 100644 cmd/workflow/delete/registry_delete_strategy.go create mode 100644 cmd/workflow/delete/registry_delete_strategy_onchain.go create mode 100644 cmd/workflow/delete/registry_delete_strategy_private.go diff --git a/cmd/workflow/delete/delete.go b/cmd/workflow/delete/delete.go index 2da19635..d1698e83 100644 --- a/cmd/workflow/delete/delete.go +++ b/cmd/workflow/delete/delete.go @@ -1,26 +1,18 @@ package delete import ( - "encoding/hex" - "errors" "fmt" "io" - "math/big" - "sync" - "time" - "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/smartcontractkit/cre-cli/cmd/client" - cmdCommon "github.com/smartcontractkit/cre-cli/cmd/common" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/settings" - "github.com/smartcontractkit/cre-cli/internal/types" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" ) @@ -29,9 +21,6 @@ type Inputs struct { WorkflowName string `validate:"workflow_name"` WorkflowOwner string `validate:"workflow_owner"` SkipConfirmation bool - - WorkflowRegistryContractAddress string `validate:"required"` - WorkflowRegistryContractChainName string `validate:"required"` } func New(runtimeContext *runtime.Context) *cobra.Command { @@ -72,13 +61,9 @@ type handler struct { credentials *credentials.Credentials environmentSet *environments.EnvironmentSet inputs Inputs - wrc *client.WorkflowRegistryV2Client runtimeContext *runtime.Context validated bool - - wg sync.WaitGroup - wrcErr error } func newHandler(ctx *runtime.Context, stdin io.Reader) *handler { @@ -92,37 +77,44 @@ func newHandler(ctx *runtime.Context, stdin io.Reader) *handler { environmentSet: ctx.EnvironmentSet, runtimeContext: ctx, validated: false, - wg: sync.WaitGroup{}, - wrcErr: nil, } - h.wg.Add(1) - go func() { - defer h.wg.Done() - wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() - if err != nil { - h.wrcErr = fmt.Errorf("failed to create workflow registry client: %w", err) - return - } - h.wrc = wrc - }() return &h } func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { - oc, err := settings.AsOnChain(h.runtimeContext.ResolvedRegistry, "delete") + resolvedWorkflowOwner, err := h.resolveWorkflowOwner() if err != nil { - return Inputs{}, err + return Inputs{}, fmt.Errorf("failed to resolve workflow owner: %w", err) } + return Inputs{ - WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, - WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, - SkipConfirmation: v.GetBool(settings.Flags.SkipConfirmation.Name), - WorkflowRegistryContractChainName: oc.ChainName(), - WorkflowRegistryContractAddress: oc.Address(), + WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, + WorkflowOwner: resolvedWorkflowOwner, + SkipConfirmation: v.GetBool(settings.Flags.SkipConfirmation.Name), }, nil } +// resolveWorkflowOwner returns the effective owner address for workflow ID computation. +// For private registry deploys, the derived workflow owner from the runtime context is used. +// For onchain deploys, the configured WorkflowOwner address is used directly. +func (h *handler) resolveWorkflowOwner() (string, error) { + if h.runtimeContext.ResolvedRegistry.Type() != settings.RegistryTypeOffChain { + return h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, nil + } + + owner := h.runtimeContext.DerivedWorkflowOwner + if owner == "" { + return "", fmt.Errorf("derived workflow owner is not available; ensure authentication succeeded") + } + + if len(owner) >= 2 && owner[:2] != "0x" { + owner = "0x" + owner + } + + return owner, nil +} + func (h *handler) ValidateInputs() error { validate, err := validation.NewValidator() if err != nil { @@ -138,130 +130,12 @@ func (h *handler) ValidateInputs() error { } func (h *handler) Execute() error { - workflowName := h.inputs.WorkflowName - workflowOwner := common.HexToAddress(h.inputs.WorkflowOwner) - - h.displayWorkflowDetails() - - h.wg.Wait() - if h.wrcErr != nil { - return h.wrcErr - } - - allWorkflows, err := h.wrc.GetWorkflowListByOwnerAndName(workflowOwner, workflowName, big.NewInt(0), big.NewInt(100)) - if err != nil { - return fmt.Errorf("failed to get workflow list: %w", err) - } - if len(allWorkflows) == 0 { - ui.Warning(fmt.Sprintf("No workflows found for name: %s", workflowName)) - return nil - } - - // Note: The way deploy is set up, there will only ever be one workflow in the command for now - h.runtimeContext.Workflow.ID = hex.EncodeToString(allWorkflows[0].WorkflowId[:]) - - ui.Bold(fmt.Sprintf("Found %d workflow(s) to delete for name: %s", len(allWorkflows), workflowName)) - for i, wf := range allWorkflows { - status := map[uint8]string{0: "ACTIVE", 1: "PAUSED"}[wf.Status] - ui.Print(fmt.Sprintf(" %d. Workflow", i+1)) - ui.Dim(fmt.Sprintf(" ID: %s", hex.EncodeToString(wf.WorkflowId[:]))) - ui.Dim(fmt.Sprintf(" Owner: %s", wf.Owner.Hex())) - ui.Dim(fmt.Sprintf(" DON Family: %s", wf.DonFamily)) - ui.Dim(fmt.Sprintf(" Tag: %s", wf.Tag)) - ui.Dim(fmt.Sprintf(" Binary URL: %s", wf.BinaryUrl)) - ui.Dim(fmt.Sprintf(" Workflow Status: %s", status)) - ui.Line() - } - - shouldDeleteWorkflow, err := h.shouldDeleteWorkflow(h.inputs.SkipConfirmation, workflowName) + adapter, err := newRegistryDeleteStrategy(h.runtimeContext.ResolvedRegistry, h) if err != nil { return err } - if !shouldDeleteWorkflow { - ui.Warning("Workflow deletion canceled") - return nil - } - - ui.Dim(fmt.Sprintf("Deleting %d workflow(s)...", len(allWorkflows))) - var errs []error - for _, wf := range allWorkflows { - txOut, err := h.wrc.DeleteWorkflow(wf.WorkflowId) - if err != nil { - h.log.Error(). - Err(err). - Str("workflowId", hex.EncodeToString(wf.WorkflowId[:])). - Msg("Failed to delete workflow") - errs = append(errs, err) - continue - } - oc, _ := h.runtimeContext.ResolvedRegistry.(*settings.OnChainRegistry) - - switch txOut.Type { - case client.Regular: - ui.Success("Transaction confirmed") - ui.URL(fmt.Sprintf("%s/tx/%s", oc.ExplorerURL(), txOut.Hash)) - ui.Success(fmt.Sprintf("Deleted workflow ID: %s", hex.EncodeToString(wf.WorkflowId[:]))) - - case client.Raw: - ui.Line() - ui.Success("MSIG workflow deletion transaction prepared!") - ui.Line() - ui.Bold("Next steps:") - ui.Line() - ui.Print(" 1. Submit the following transaction on the target chain:") - ui.Dim(fmt.Sprintf(" Chain: %s", h.inputs.WorkflowRegistryContractChainName)) - ui.Dim(fmt.Sprintf(" Contract Address: %s", txOut.RawTx.To)) - ui.Line() - ui.Print(" 2. Use the following transaction data:") - ui.Line() - ui.Code(fmt.Sprintf(" %x", txOut.RawTx.Data)) - ui.Line() - - case client.Changeset: - chainSelector, err := settings.GetChainSelectorByChainName(oc.ChainName()) - if err != nil { - return fmt.Errorf("failed to get chain selector for chain %q: %w", oc.ChainName(), err) - } - mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) - if err != nil { - ui.Warning("MCMS config not found or is incorrect, skipping MCMS config in changeset") - } - cldSettings := h.settings.CLDSettings - changesets := []types.Changeset{ - { - DeleteWorkflow: &types.DeleteWorkflow{ - Payload: types.UserWorkflowDeleteInput{ - WorkflowID: h.runtimeContext.Workflow.ID, - - ChainSelector: chainSelector, - MCMSConfig: mcmsConfig, - WorkflowRegistryQualifier: cldSettings.WorkflowRegistryQualifier, - }, - }, - }, - } - csFile := types.NewChangesetFile(cldSettings.Environment, cldSettings.Domain, cldSettings.MergeProposals, changesets) - - var fileName string - if cldSettings.ChangesetFile != "" { - fileName = cldSettings.ChangesetFile - } else { - fileName = fmt.Sprintf("DeleteWorkflow_%s_%s.yaml", workflowName, time.Now().Format("20060102_150405")) - } - return cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) - - default: - h.log.Warn().Msgf("Unsupported transaction type: %s", txOut.Type) - } - - // Workflow artifacts deletion will be handled by a background cleanup process. - } - if len(errs) > 0 { - return fmt.Errorf("failed to delete some workflows: %w", errors.Join(errs...)) - } - ui.Success("Workflows deleted successfully") - return nil + return adapter.Delete() } func (h *handler) shouldDeleteWorkflow(skipConfirmation bool, workflowName string) (bool, error) { @@ -293,6 +167,6 @@ func (h *handler) displayWorkflowDetails() { ui.Line() ui.Title(fmt.Sprintf("Deleting Workflow: %s", h.inputs.WorkflowName)) ui.Dim(fmt.Sprintf("Target: %s", h.settings.User.TargetName)) - ui.Dim(fmt.Sprintf("Owner Address: %s", h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress)) + ui.Dim(fmt.Sprintf("Owner Address: %s", h.inputs.WorkflowOwner)) ui.Line() } diff --git a/cmd/workflow/delete/delete_test.go b/cmd/workflow/delete/delete_test.go index 55ea63f7..dbdc07ad 100644 --- a/cmd/workflow/delete/delete_test.go +++ b/cmd/workflow/delete/delete_test.go @@ -88,13 +88,6 @@ func TestWorkflowDeleteCommand(t *testing.T) { } ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA - if tt.inputs.WorkflowRegistryContractAddress == "" { - tt.inputs.WorkflowRegistryContractAddress = "0x0000000000000000000000000000000000000000" - } - if tt.inputs.WorkflowRegistryContractChainName == "" { - tt.inputs.WorkflowRegistryContractChainName = "ethereum-testnet-sepolia" - } - handler := newHandler(ctx, testutil.EmptyMockStdinReader()) handler.inputs = tt.inputs diff --git a/cmd/workflow/delete/registry_delete_strategy.go b/cmd/workflow/delete/registry_delete_strategy.go new file mode 100644 index 00000000..4623c090 --- /dev/null +++ b/cmd/workflow/delete/registry_delete_strategy.go @@ -0,0 +1,18 @@ +package delete + +import ( + "github.com/smartcontractkit/cre-cli/internal/settings" +) + +// registryDeleteStrategy encapsulates target-specific delete logic. +type registryDeleteStrategy interface { + Delete() error +} + +// newRegistryDeleteStrategy returns the appropriate strategy for the given target. +func newRegistryDeleteStrategy(resolvedRegistry settings.ResolvedRegistry, h *handler) (registryDeleteStrategy, error) { + if resolvedRegistry.Type() == settings.RegistryTypeOffChain { + return newPrivateRegistryDeleteStrategy(h), nil + } + return newOnchainRegistryDeleteStrategy(h) +} diff --git a/cmd/workflow/delete/registry_delete_strategy_onchain.go b/cmd/workflow/delete/registry_delete_strategy_onchain.go new file mode 100644 index 00000000..0b776a5e --- /dev/null +++ b/cmd/workflow/delete/registry_delete_strategy_onchain.go @@ -0,0 +1,173 @@ +package delete + +import ( + "encoding/hex" + "errors" + "fmt" + "math/big" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/cre-cli/cmd/client" + cmdCommon "github.com/smartcontractkit/cre-cli/cmd/common" + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/types" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +type onchainRegistryDeleteStrategy struct { + h *handler + wrc *client.WorkflowRegistryV2Client + onChain *settings.OnChainRegistry + wg sync.WaitGroup + initErr error +} + +func newOnchainRegistryDeleteStrategy(h *handler) (*onchainRegistryDeleteStrategy, error) { + onChain, err := settings.AsOnChain(h.runtimeContext.ResolvedRegistry, "delete") + if err != nil { + return nil, err + } + + a := &onchainRegistryDeleteStrategy{h: h, onChain: onChain} + a.wg.Add(1) + go func() { + defer a.wg.Done() + wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() + if err != nil { + a.initErr = fmt.Errorf("failed to create workflow registry client: %w", err) + return + } + a.wrc = wrc + }() + return a, nil +} + +func (a *onchainRegistryDeleteStrategy) Delete() error { + h := a.h + + a.wg.Wait() + if a.initErr != nil { + return a.initErr + } + + workflowName := h.inputs.WorkflowName + workflowOwner := common.HexToAddress(h.inputs.WorkflowOwner) + + allWorkflows, err := a.wrc.GetWorkflowListByOwnerAndName(workflowOwner, workflowName, big.NewInt(0), big.NewInt(100)) + if err != nil { + return fmt.Errorf("failed to get workflow list: %w", err) + } + if len(allWorkflows) == 0 { + ui.Warning(fmt.Sprintf("No workflows found for name: %s", workflowName)) + return nil + } + + // Note: The way deploy is set up, there will only ever be one workflow in the command for now + h.runtimeContext.Workflow.ID = hex.EncodeToString(allWorkflows[0].WorkflowId[:]) + + ui.Bold(fmt.Sprintf("Found %d workflow(s) to delete for name: %s", len(allWorkflows), workflowName)) + for i, wf := range allWorkflows { + status := map[uint8]string{0: "ACTIVE", 1: "PAUSED"}[wf.Status] + ui.Print(fmt.Sprintf(" %d. Workflow", i+1)) + ui.Dim(fmt.Sprintf(" ID: %s", hex.EncodeToString(wf.WorkflowId[:]))) + ui.Dim(fmt.Sprintf(" Owner: %s", wf.Owner.Hex())) + ui.Dim(fmt.Sprintf(" DON Family: %s", wf.DonFamily)) + ui.Dim(fmt.Sprintf(" Tag: %s", wf.Tag)) + ui.Dim(fmt.Sprintf(" Binary URL: %s", wf.BinaryUrl)) + ui.Dim(fmt.Sprintf(" Workflow Status: %s", status)) + ui.Line() + } + + shouldDeleteWorkflow, err := h.shouldDeleteWorkflow(h.inputs.SkipConfirmation, workflowName) + if err != nil { + return err + } + if !shouldDeleteWorkflow { + ui.Warning("Workflow deletion canceled") + return nil + } + + ui.Dim(fmt.Sprintf("Deleting %d workflow(s)...", len(allWorkflows))) + var errs []error + for _, wf := range allWorkflows { + txOut, err := a.wrc.DeleteWorkflow(wf.WorkflowId) + if err != nil { + h.log.Error(). + Err(err). + Str("workflowId", hex.EncodeToString(wf.WorkflowId[:])). + Msg("Failed to delete workflow") + errs = append(errs, err) + continue + } + oc := a.onChain + + switch txOut.Type { + case client.Regular: + ui.Success("Transaction confirmed") + ui.URL(fmt.Sprintf("%s/tx/%s", oc.ExplorerURL(), txOut.Hash)) + ui.Success(fmt.Sprintf("Deleted workflow ID: %s", hex.EncodeToString(wf.WorkflowId[:]))) + + case client.Raw: + ui.Line() + ui.Success("MSIG workflow deletion transaction prepared!") + ui.Line() + ui.Bold("Next steps:") + ui.Line() + ui.Print(" 1. Submit the following transaction on the target chain:") + ui.Dim(fmt.Sprintf(" Chain: %s", oc.ChainName())) + ui.Dim(fmt.Sprintf(" Contract Address: %s", txOut.RawTx.To)) + ui.Line() + ui.Print(" 2. Use the following transaction data:") + ui.Line() + ui.Code(fmt.Sprintf(" %x", txOut.RawTx.Data)) + ui.Line() + + case client.Changeset: + chainSelector, err := settings.GetChainSelectorByChainName(oc.ChainName()) + if err != nil { + return fmt.Errorf("failed to get chain selector for chain %q: %w", oc.ChainName(), err) + } + mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) + if err != nil { + ui.Warning("MCMS config not found or is incorrect, skipping MCMS config in changeset") + } + cldSettings := h.settings.CLDSettings + changesets := []types.Changeset{ + { + DeleteWorkflow: &types.DeleteWorkflow{ + Payload: types.UserWorkflowDeleteInput{ + WorkflowID: h.runtimeContext.Workflow.ID, + + ChainSelector: chainSelector, + MCMSConfig: mcmsConfig, + WorkflowRegistryQualifier: cldSettings.WorkflowRegistryQualifier, + }, + }, + }, + } + csFile := types.NewChangesetFile(cldSettings.Environment, cldSettings.Domain, cldSettings.MergeProposals, changesets) + + var fileName string + if cldSettings.ChangesetFile != "" { + fileName = cldSettings.ChangesetFile + } else { + fileName = fmt.Sprintf("DeleteWorkflow_%s_%s.yaml", workflowName, time.Now().Format("20060102_150405")) + } + + return cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) + + default: + h.log.Warn().Msgf("Unsupported transaction type: %s", txOut.Type) + } + + // Workflow artifacts deletion will be handled by a background cleanup process. + } + if len(errs) > 0 { + return fmt.Errorf("failed to delete some workflows: %w", errors.Join(errs...)) + } + ui.Success("Workflows deleted successfully") + return nil +} diff --git a/cmd/workflow/delete/registry_delete_strategy_private.go b/cmd/workflow/delete/registry_delete_strategy_private.go new file mode 100644 index 00000000..dbe0b898 --- /dev/null +++ b/cmd/workflow/delete/registry_delete_strategy_private.go @@ -0,0 +1,76 @@ +package delete + +import ( + "fmt" + + "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" + "github.com/smartcontractkit/cre-cli/internal/client/privateregistryclient" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +type privateRegistryDeleteStrategy struct { + h *handler + prc *privateregistryclient.Client +} + +func newPrivateRegistryDeleteStrategy(h *handler) *privateRegistryDeleteStrategy { + return &privateRegistryDeleteStrategy{h: h} +} + +func (a *privateRegistryDeleteStrategy) ensureClient() { + if a.prc == nil { + gql := graphqlclient.New(a.h.runtimeContext.Credentials, a.h.environmentSet, a.h.log) + a.prc = privateregistryclient.New(gql, a.h.log) + } +} + +func (a *privateRegistryDeleteStrategy) Delete() error { + a.ensureClient() + + h := a.h + workflowName := h.inputs.WorkflowName + + ui.Dim(fmt.Sprintf("Fetching workflow to delete... Name=%s", workflowName)) + + workflow, err := a.prc.GetWorkflowByName(workflowName) + if err != nil { + return fmt.Errorf("failed to get workflow: %w", err) + } + + h.runtimeContext.Workflow.ID = workflow.WorkflowID + + ui.Bold(fmt.Sprintf("Found 1 workflow(s) to delete for name: %s", workflowName)) + ui.Print(" 1. Workflow") + ui.Dim(fmt.Sprintf(" ID: %s", workflow.WorkflowID)) + ui.Dim(fmt.Sprintf(" Owner: %s", workflow.Owner)) + ui.Dim(fmt.Sprintf(" DON Family: %s", workflow.DonFamily)) + ui.Dim(fmt.Sprintf(" Tag: %s", workflow.Tag)) + ui.Dim(fmt.Sprintf(" Binary URL: %s", workflow.BinaryUrl)) + ui.Dim(fmt.Sprintf(" Workflow Status: %s", workflow.Status)) + ui.Line() + + shouldDeleteWorkflow, err := h.shouldDeleteWorkflow(h.inputs.SkipConfirmation, workflowName) + if err != nil { + return err + } + if !shouldDeleteWorkflow { + ui.Warning("Workflow deletion canceled") + return nil + } + + ui.Dim("Deleting 1 workflow(s)...") + + deletedID, err := a.prc.DeleteWorkflowInRegistry(workflow.WorkflowID) + if err != nil { + h.log.Error(). + Err(err). + Str("workflowId", workflow.WorkflowID). + Msg("Failed to delete workflow") + return fmt.Errorf("failed to delete workflow in private registry: %w", err) + } + + ui.Success(fmt.Sprintf("Deleted workflow ID: %s", deletedID)) + ui.Success("Workflows deleted successfully") + + return nil +} diff --git a/test/multi_command_flows/workflow_private_registry.go b/test/multi_command_flows/workflow_private_registry.go index 4d8a4be4..d26e0705 100644 --- a/test/multi_command_flows/workflow_private_registry.go +++ b/test/multi_command_flows/workflow_private_registry.go @@ -640,3 +640,162 @@ func RunWorkflowActivatePrivateRegistryHappyPath(t *testing.T, tc TestConfig) { require.Contains(t, out, "Status: WORKFLOW_STATUS_ACTIVE", "expected active status in details.\nCLI OUTPUT:\n%s", out) require.Contains(t, out, "Owner: "+privateRegistryOwnerAddress, "expected owner in details.\nCLI OUTPUT:\n%s", out) } + +// workflowDeletePrivateRegistry deletes a workflow in the private registry via CLI +// using a mock GraphQL server. +func workflowDeletePrivateRegistry(t *testing.T, tc TestConfig) string { + t.Helper() + + var getWorkflowCalled atomic.Bool + var deleteWorkflowCalled atomic.Bool + var srv *httptest.Server + srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasPrefix(r.URL.Path, "/graphql") && r.Method == http.MethodPost: + var req graphQLRequest + _ = json.NewDecoder(r.Body).Decode(&req) + + w.Header().Set("Content-Type", "application/json") + + if strings.Contains(req.Query, "getCreOrganizationInfo") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "GetTenantConfig") || strings.Contains(req.Query, "getTenantConfig") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getTenantConfig": map[string]any{ + "tenantId": "42", + "defaultDonFamily": "test-don", + "vaultGatewayUrl": "https://vault.example.test", + "registries": []map[string]any{ + { + "id": "reg-test", + "label": "reg-test", + "type": "OFF_CHAIN", + "chainSelector": "6433500567565415381", + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "secretsAuthFlows": []string{"BROWSER"}, + }, + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "GetOffchainWorkflowByName") { + getWorkflowCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getOffchainWorkflowByName": map[string]any{ + "workflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "owner": privateRegistryOwnerAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "private-registry-happy-path-workflow", + "binaryUrl": srv.URL + "/get/binary.wasm", + "configUrl": "", + "tag": "private-registry-happy-path-workflow", + "attributes": "", + "donFamily": "test-don", + "organizationId": "test-org-id", + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "DeleteOffchainWorkflow") { + deleteWorkflowCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "deleteOffchainWorkflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + }, + }, + }) + return + } + + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "errors": []map[string]string{{"message": "Unsupported GraphQL query"}}, + }) + return + + default: + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("not found")) + return + } + })) + defer srv.Close() + + t.Setenv(environments.EnvVarGraphQLURL, srv.URL+"/graphql") + + args := []string{ + "workflow", "delete", + "blank_workflow", + tc.GetCliEnvFlag(), + tc.GetProjectRootFlag(), + "--" + settings.Flags.SkipConfirmation.Name, + } + + cmd := exec.Command(CLIPath, args...) + testHome := createTestBearerCredentialsHome(t) + + realHome, err := os.UserHomeDir() + require.NoError(t, err, "failed to get real home dir") + + childEnv := make([]string, 0, len(os.Environ())+3) + hasGOPATH := false + for _, entry := range os.Environ() { + if strings.HasPrefix(entry, "HOME=") || strings.HasPrefix(entry, "USERPROFILE=") { + continue + } + if strings.HasPrefix(entry, "GOPATH=") { + hasGOPATH = true + } + childEnv = append(childEnv, entry) + } + childEnv = append(childEnv, "HOME="+testHome, "USERPROFILE="+testHome) + if !hasGOPATH { + childEnv = append(childEnv, "GOPATH="+filepath.Join(realHome, "go")) + } + cmd.Env = childEnv + + var stdout, stderr bytes.Buffer + cmd.Stdout, cmd.Stderr = &stdout, &stderr + + require.NoError( + t, + cmd.Run(), + "cre workflow delete failed:\nSTDOUT:\n%s\nSTDERR:\n%s", + stdout.String(), + stderr.String(), + ) + require.True(t, getWorkflowCalled.Load(), "expected GetOffchainWorkflowByName to be called") + require.True(t, deleteWorkflowCalled.Load(), "expected DeleteOffchainWorkflow to be called") + + return StripANSI(stdout.String() + stderr.String()) +} + +// RunWorkflowDeletePrivateRegistryHappyPath runs the workflow delete happy path for private registry. +func RunWorkflowDeletePrivateRegistryHappyPath(t *testing.T, tc TestConfig) { + t.Helper() + + out := workflowDeletePrivateRegistry(t, tc) + require.Contains(t, out, "Workflows deleted successfully", "expected private registry delete success.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Deleted workflow ID: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "expected workflow ID in details.\nCLI OUTPUT:\n%s", out) +} diff --git a/test/multi_command_test.go b/test/multi_command_test.go index 26af85a9..f3aab075 100644 --- a/test/multi_command_test.go +++ b/test/multi_command_test.go @@ -144,6 +144,7 @@ func TestMultiCommandHappyPaths(t *testing.T) { multi_command_flows.RunWorkflowPrivateRegistryHappyPath(t, tc) multi_command_flows.RunWorkflowPausePrivateRegistryHappyPath(t, tc) multi_command_flows.RunWorkflowActivatePrivateRegistryHappyPath(t, tc) + multi_command_flows.RunWorkflowDeletePrivateRegistryHappyPath(t, tc) }) // Run Account Happy Path: Link -> List -> Unlink -> List (verify unlinked) From 8ec9a683d153c629b19975eb6dae9e9efb3e5413 Mon Sep 17 00:00:00 2001 From: Jakub Nowak Date: Fri, 17 Apr 2026 15:47:01 +0200 Subject: [PATCH 08/10] refactor --- cmd/workflow/delete/delete.go | 46 ++++++++++++- .../delete/registry_delete_strategy.go | 13 +++- .../registry_delete_strategy_onchain.go | 66 ++++++++----------- .../registry_delete_strategy_private.go | 61 ++++++++--------- 4 files changed, 114 insertions(+), 72 deletions(-) diff --git a/cmd/workflow/delete/delete.go b/cmd/workflow/delete/delete.go index d1698e83..a9ca8919 100644 --- a/cmd/workflow/delete/delete.go +++ b/cmd/workflow/delete/delete.go @@ -135,7 +135,51 @@ func (h *handler) Execute() error { return err } - return adapter.Delete() + h.displayWorkflowDetails() + + workflows, err := adapter.FetchWorkflows() + if err != nil { + return err + } + + if len(workflows) == 0 { + ui.Warning(fmt.Sprintf("No workflows found for name: %s", h.inputs.WorkflowName)) + return nil + } + + // Note: The way deploy is set up, there will only ever be one workflow in the command for now + h.runtimeContext.Workflow.ID = workflows[0].ID + + ui.Bold(fmt.Sprintf("Found %d workflow(s) to delete for name: %s", len(workflows), h.inputs.WorkflowName)) + for i, wf := range workflows { + ui.Print(fmt.Sprintf(" %d. Workflow", i+1)) + ui.Dim(fmt.Sprintf(" ID: %s", wf.ID)) + ui.Dim(fmt.Sprintf(" Owner: %s", wf.Owner)) + ui.Dim(fmt.Sprintf(" DON Family: %s", wf.DonFamily)) + ui.Dim(fmt.Sprintf(" Tag: %s", wf.Tag)) + ui.Dim(fmt.Sprintf(" Binary URL: %s", wf.BinaryURL)) + ui.Dim(fmt.Sprintf(" Workflow Status: %s", wf.Status)) + ui.Line() + } + + shouldDeleteWorkflow, err := h.shouldDeleteWorkflow(h.inputs.SkipConfirmation, h.inputs.WorkflowName) + if err != nil { + return err + } + if !shouldDeleteWorkflow { + ui.Warning("Workflow deletion canceled") + return nil + } + + ui.Dim(fmt.Sprintf("Deleting %d workflow(s)...", len(workflows))) + + err = adapter.DeleteWorkflows(workflows) + if err != nil { + return err + } + + ui.Success("Workflows deleted successfully") + return nil } func (h *handler) shouldDeleteWorkflow(skipConfirmation bool, workflowName string) (bool, error) { diff --git a/cmd/workflow/delete/registry_delete_strategy.go b/cmd/workflow/delete/registry_delete_strategy.go index 4623c090..3e347565 100644 --- a/cmd/workflow/delete/registry_delete_strategy.go +++ b/cmd/workflow/delete/registry_delete_strategy.go @@ -4,9 +4,20 @@ import ( "github.com/smartcontractkit/cre-cli/internal/settings" ) +type WorkflowToDelete struct { + ID string + Owner string + DonFamily string + Tag string + BinaryURL string + Status string + RawID any // Holds the registry-specific ID type ([32]byte for on-chain, string for private) +} + // registryDeleteStrategy encapsulates target-specific delete logic. type registryDeleteStrategy interface { - Delete() error + FetchWorkflows() ([]WorkflowToDelete, error) + DeleteWorkflows(workflows []WorkflowToDelete) error } // newRegistryDeleteStrategy returns the appropriate strategy for the given target. diff --git a/cmd/workflow/delete/registry_delete_strategy_onchain.go b/cmd/workflow/delete/registry_delete_strategy_onchain.go index 0b776a5e..1dd7335b 100644 --- a/cmd/workflow/delete/registry_delete_strategy_onchain.go +++ b/cmd/workflow/delete/registry_delete_strategy_onchain.go @@ -45,12 +45,12 @@ func newOnchainRegistryDeleteStrategy(h *handler) (*onchainRegistryDeleteStrateg return a, nil } -func (a *onchainRegistryDeleteStrategy) Delete() error { +func (a *onchainRegistryDeleteStrategy) FetchWorkflows() ([]WorkflowToDelete, error) { h := a.h a.wg.Wait() if a.initErr != nil { - return a.initErr + return nil, a.initErr } workflowName := h.inputs.WorkflowName @@ -58,46 +58,36 @@ func (a *onchainRegistryDeleteStrategy) Delete() error { allWorkflows, err := a.wrc.GetWorkflowListByOwnerAndName(workflowOwner, workflowName, big.NewInt(0), big.NewInt(100)) if err != nil { - return fmt.Errorf("failed to get workflow list: %w", err) + return nil, fmt.Errorf("failed to get workflow list: %w", err) } - if len(allWorkflows) == 0 { - ui.Warning(fmt.Sprintf("No workflows found for name: %s", workflowName)) - return nil - } - - // Note: The way deploy is set up, there will only ever be one workflow in the command for now - h.runtimeContext.Workflow.ID = hex.EncodeToString(allWorkflows[0].WorkflowId[:]) - ui.Bold(fmt.Sprintf("Found %d workflow(s) to delete for name: %s", len(allWorkflows), workflowName)) - for i, wf := range allWorkflows { + var workflows []WorkflowToDelete + for _, wf := range allWorkflows { status := map[uint8]string{0: "ACTIVE", 1: "PAUSED"}[wf.Status] - ui.Print(fmt.Sprintf(" %d. Workflow", i+1)) - ui.Dim(fmt.Sprintf(" ID: %s", hex.EncodeToString(wf.WorkflowId[:]))) - ui.Dim(fmt.Sprintf(" Owner: %s", wf.Owner.Hex())) - ui.Dim(fmt.Sprintf(" DON Family: %s", wf.DonFamily)) - ui.Dim(fmt.Sprintf(" Tag: %s", wf.Tag)) - ui.Dim(fmt.Sprintf(" Binary URL: %s", wf.BinaryUrl)) - ui.Dim(fmt.Sprintf(" Workflow Status: %s", status)) - ui.Line() + workflows = append(workflows, WorkflowToDelete{ + ID: hex.EncodeToString(wf.WorkflowId[:]), + Owner: wf.Owner.Hex(), + DonFamily: wf.DonFamily, + Tag: wf.Tag, + BinaryURL: wf.BinaryUrl, + Status: status, + RawID: wf.WorkflowId, + }) } - shouldDeleteWorkflow, err := h.shouldDeleteWorkflow(h.inputs.SkipConfirmation, workflowName) - if err != nil { - return err - } - if !shouldDeleteWorkflow { - ui.Warning("Workflow deletion canceled") - return nil - } + return workflows, nil +} - ui.Dim(fmt.Sprintf("Deleting %d workflow(s)...", len(allWorkflows))) +func (a *onchainRegistryDeleteStrategy) DeleteWorkflows(workflows []WorkflowToDelete) error { + h := a.h var errs []error - for _, wf := range allWorkflows { - txOut, err := a.wrc.DeleteWorkflow(wf.WorkflowId) + for _, wf := range workflows { + workflowID := wf.RawID.([32]byte) + txOut, err := a.wrc.DeleteWorkflow(workflowID) if err != nil { h.log.Error(). Err(err). - Str("workflowId", hex.EncodeToString(wf.WorkflowId[:])). + Str("workflowId", wf.ID). Msg("Failed to delete workflow") errs = append(errs, err) continue @@ -108,7 +98,7 @@ func (a *onchainRegistryDeleteStrategy) Delete() error { case client.Regular: ui.Success("Transaction confirmed") ui.URL(fmt.Sprintf("%s/tx/%s", oc.ExplorerURL(), txOut.Hash)) - ui.Success(fmt.Sprintf("Deleted workflow ID: %s", hex.EncodeToString(wf.WorkflowId[:]))) + ui.Success(fmt.Sprintf("Deleted workflow ID: %s", wf.ID)) case client.Raw: ui.Line() @@ -139,7 +129,7 @@ func (a *onchainRegistryDeleteStrategy) Delete() error { { DeleteWorkflow: &types.DeleteWorkflow{ Payload: types.UserWorkflowDeleteInput{ - WorkflowID: h.runtimeContext.Workflow.ID, + WorkflowID: wf.ID, ChainSelector: chainSelector, MCMSConfig: mcmsConfig, @@ -154,10 +144,13 @@ func (a *onchainRegistryDeleteStrategy) Delete() error { if cldSettings.ChangesetFile != "" { fileName = cldSettings.ChangesetFile } else { - fileName = fmt.Sprintf("DeleteWorkflow_%s_%s.yaml", workflowName, time.Now().Format("20060102_150405")) + fileName = fmt.Sprintf("DeleteWorkflow_%s_%s.yaml", h.inputs.WorkflowName, time.Now().Format("20060102_150405")) } - return cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) + err = cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) + if err != nil { + return err + } default: h.log.Warn().Msgf("Unsupported transaction type: %s", txOut.Type) @@ -168,6 +161,5 @@ func (a *onchainRegistryDeleteStrategy) Delete() error { if len(errs) > 0 { return fmt.Errorf("failed to delete some workflows: %w", errors.Join(errs...)) } - ui.Success("Workflows deleted successfully") return nil } diff --git a/cmd/workflow/delete/registry_delete_strategy_private.go b/cmd/workflow/delete/registry_delete_strategy_private.go index dbe0b898..f85ca7ba 100644 --- a/cmd/workflow/delete/registry_delete_strategy_private.go +++ b/cmd/workflow/delete/registry_delete_strategy_private.go @@ -24,7 +24,7 @@ func (a *privateRegistryDeleteStrategy) ensureClient() { } } -func (a *privateRegistryDeleteStrategy) Delete() error { +func (a *privateRegistryDeleteStrategy) FetchWorkflows() ([]WorkflowToDelete, error) { a.ensureClient() h := a.h @@ -34,43 +34,38 @@ func (a *privateRegistryDeleteStrategy) Delete() error { workflow, err := a.prc.GetWorkflowByName(workflowName) if err != nil { - return fmt.Errorf("failed to get workflow: %w", err) + return nil, fmt.Errorf("failed to get workflow: %w", err) } - h.runtimeContext.Workflow.ID = workflow.WorkflowID - - ui.Bold(fmt.Sprintf("Found 1 workflow(s) to delete for name: %s", workflowName)) - ui.Print(" 1. Workflow") - ui.Dim(fmt.Sprintf(" ID: %s", workflow.WorkflowID)) - ui.Dim(fmt.Sprintf(" Owner: %s", workflow.Owner)) - ui.Dim(fmt.Sprintf(" DON Family: %s", workflow.DonFamily)) - ui.Dim(fmt.Sprintf(" Tag: %s", workflow.Tag)) - ui.Dim(fmt.Sprintf(" Binary URL: %s", workflow.BinaryUrl)) - ui.Dim(fmt.Sprintf(" Workflow Status: %s", workflow.Status)) - ui.Line() - - shouldDeleteWorkflow, err := h.shouldDeleteWorkflow(h.inputs.SkipConfirmation, workflowName) - if err != nil { - return err - } - if !shouldDeleteWorkflow { - ui.Warning("Workflow deletion canceled") - return nil - } + return []WorkflowToDelete{ + { + ID: workflow.WorkflowID, + Owner: workflow.Owner, + DonFamily: workflow.DonFamily, + Tag: workflow.Tag, + BinaryURL: workflow.BinaryUrl, + Status: workflow.Status, + RawID: workflow.WorkflowID, + }, + }, nil +} - ui.Dim("Deleting 1 workflow(s)...") +func (a *privateRegistryDeleteStrategy) DeleteWorkflows(workflows []WorkflowToDelete) error { + h := a.h - deletedID, err := a.prc.DeleteWorkflowInRegistry(workflow.WorkflowID) - if err != nil { - h.log.Error(). - Err(err). - Str("workflowId", workflow.WorkflowID). - Msg("Failed to delete workflow") - return fmt.Errorf("failed to delete workflow in private registry: %w", err) + for _, wf := range workflows { + workflowID := wf.RawID.(string) + deletedID, err := a.prc.DeleteWorkflowInRegistry(workflowID) + if err != nil { + h.log.Error(). + Err(err). + Str("workflowId", workflowID). + Msg("Failed to delete workflow") + return fmt.Errorf("failed to delete workflow in private registry: %w", err) + } + + ui.Success(fmt.Sprintf("Deleted workflow ID: %s", deletedID)) } - ui.Success(fmt.Sprintf("Deleted workflow ID: %s", deletedID)) - ui.Success("Workflows deleted successfully") - return nil } From 26231a9a8b9b78f3e6c608fa010642aee8018fc2 Mon Sep 17 00:00:00 2001 From: Jakub Nowak Date: Fri, 17 Apr 2026 15:48:54 +0200 Subject: [PATCH 09/10] linter --- cmd/workflow/delete/registry_delete_strategy_private.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/workflow/delete/registry_delete_strategy_private.go b/cmd/workflow/delete/registry_delete_strategy_private.go index f85ca7ba..2f81f125 100644 --- a/cmd/workflow/delete/registry_delete_strategy_private.go +++ b/cmd/workflow/delete/registry_delete_strategy_private.go @@ -43,8 +43,8 @@ func (a *privateRegistryDeleteStrategy) FetchWorkflows() ([]WorkflowToDelete, er Owner: workflow.Owner, DonFamily: workflow.DonFamily, Tag: workflow.Tag, - BinaryURL: workflow.BinaryUrl, - Status: workflow.Status, + BinaryURL: workflow.BinaryURL, + Status: string(workflow.Status), RawID: workflow.WorkflowID, }, }, nil From d643190f53d7f0a0fcac071126fcfd3a73be3e26 Mon Sep 17 00:00:00 2001 From: Jakub Nowak Date: Fri, 17 Apr 2026 16:25:45 +0200 Subject: [PATCH 10/10] linter --- cmd/workflow/delete/delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/workflow/delete/delete.go b/cmd/workflow/delete/delete.go index a9ca8919..95d35535 100644 --- a/cmd/workflow/delete/delete.go +++ b/cmd/workflow/delete/delete.go @@ -172,7 +172,7 @@ func (h *handler) Execute() error { } ui.Dim(fmt.Sprintf("Deleting %d workflow(s)...", len(workflows))) - + err = adapter.DeleteWorkflows(workflows) if err != nil { return err