Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 49 additions & 131 deletions cmd/workflow/delete/delete.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -138,42 +130,39 @@ func (h *handler) ValidateInputs() error {
}

func (h *handler) Execute() error {
workflowName := h.inputs.WorkflowName
workflowOwner := common.HexToAddress(h.inputs.WorkflowOwner)
adapter, err := newRegistryDeleteStrategy(h.runtimeContext.ResolvedRegistry, h)
if err != nil {
return err
}

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))
workflows, err := adapter.FetchWorkflows()
if err != nil {
return fmt.Errorf("failed to get workflow list: %w", err)
return err
}
if len(allWorkflows) == 0 {
ui.Warning(fmt.Sprintf("No workflows found for name: %s", workflowName))

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 = hex.EncodeToString(allWorkflows[0].WorkflowId[:])
h.runtimeContext.Workflow.ID = workflows[0].ID

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.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", hex.EncodeToString(wf.WorkflowId[:])))
ui.Dim(fmt.Sprintf(" Owner: %s", wf.Owner.Hex()))
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", status))
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, workflowName)
shouldDeleteWorkflow, err := h.shouldDeleteWorkflow(h.inputs.SkipConfirmation, h.inputs.WorkflowName)
if err != nil {
return err
}
Expand All @@ -182,84 +171,13 @@ func (h *handler) Execute() error {
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)
ui.Dim(fmt.Sprintf("Deleting %d workflow(s)...", len(workflows)))

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...))
err = adapter.DeleteWorkflows(workflows)
if err != nil {
return err
}

ui.Success("Workflows deleted successfully")
return nil
}
Expand Down Expand Up @@ -293,6 +211,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()
}
7 changes: 0 additions & 7 deletions cmd/workflow/delete/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
29 changes: 29 additions & 0 deletions cmd/workflow/delete/registry_delete_strategy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package delete

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 {
FetchWorkflows() ([]WorkflowToDelete, error)
DeleteWorkflows(workflows []WorkflowToDelete) 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)
}
Loading
Loading