diff --git a/cre/changesets/workflow_delete.go b/cre/changesets/workflow_delete.go new file mode 100644 index 0000000..1b77856 --- /dev/null +++ b/cre/changesets/workflow_delete.go @@ -0,0 +1,66 @@ +package changesets + +import ( + "errors" + "fmt" + "strings" + + creops "github.com/smartcontractkit/cld-changesets/cre/operations" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cfgenv "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config/env" + fwops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +// CREWorkflowDeleteChangeset writes workflow.yaml from registry/name, resolves project.yaml, and runs `cre workflow delete`. +type CREWorkflowDeleteChangeset struct{} + +// VerifyPreconditions ensures the environment can run CRE and input is valid. +func (CREWorkflowDeleteChangeset) VerifyPreconditions(e cldf.Environment, input creops.CREWorkflowDeleteInput) error { + if e.CRERunner == nil { + return errors.New("cre runner is not available in this environment") + } + if e.CRERunner.CLI() == nil { + return errors.New("cre CLI runner is not configured") + } + if err := input.Validate(); err != nil { + return err + } + if strings.TrimSpace(input.DeploymentRegistry) == "" { + return errors.New("deploymentRegistry is required") + } + if strings.TrimSpace(input.DonFamily) == "" { + return errors.New("donFamily is required") + } + + return nil +} + +// Apply loads CRE config and runs the CRE workflow delete operation. +func (CREWorkflowDeleteChangeset) Apply(e cldf.Environment, input creops.CREWorkflowDeleteInput) (cldf.ChangesetOutput, error) { + envCfg, err := cfgenv.LoadEnv() + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("load CRE env config: %w", err) + } + + deps := creops.CREDeployDeps{ + CLI: e.CRERunner.CLI(), + CRECfg: envCfg.CRE, + EVMDeployerKey: envCfg.Onchain.EVM.DeployerKey, + } + + report, err := fwops.ExecuteOperation[creops.CREWorkflowDeleteInput, creops.CREWorkflowDeleteOutput, creops.CREDeployDeps]( + e.OperationsBundle, + creops.CREWorkflowDeleteOp, + deps, + input, + ) + out := cldf.ChangesetOutput{ + Reports: []fwops.Report[any, any]{report.ToGenericReport()}, + } + if err != nil { + return out, err + } + + return out, nil +} diff --git a/cre/changesets/workflow_delete_test.go b/cre/changesets/workflow_delete_test.go new file mode 100644 index 0000000..af72be8 --- /dev/null +++ b/cre/changesets/workflow_delete_test.go @@ -0,0 +1,194 @@ +package changesets + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cld-changesets/cre/operations" + + fcre "github.com/smartcontractkit/chainlink-deployments-framework/cre" + creartifacts "github.com/smartcontractkit/chainlink-deployments-framework/cre/artifacts" + cremocks "github.com/smartcontractkit/chainlink-deployments-framework/cre/mocks" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + testenv "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" +) + +func validDeleteInput(t *testing.T) operations.CREWorkflowDeleteInput { + t.Helper() + projectPath := filepath.Join(t.TempDir(), "project.yaml") + require.NoError(t, os.WriteFile(projectPath, []byte("cld-deploy:\n cre-cli:\n don-family: zone\n"), 0o600)) + + return operations.CREWorkflowDeleteInput{ + WorkflowName: "wf", + DonFamily: "zone", + DeploymentRegistry: "private", + Project: creartifacts.NewConfigSourceLocal(projectPath), + } +} + +func TestCREWorkflowDeleteChangeset_VerifyPreconditions(t *testing.T) { + t.Parallel() + + mockCLI := cremocks.NewMockCLIRunner(t) + envNoCLI := newTestEnv(t, testenv.WithCRERunner(fcre.NewRunner())) + envWithCLI := newTestEnv(t, testenv.WithCRERunner(fcre.NewRunner(fcre.WithCLI(mockCLI)))) + envNoCRE := newTestEnv(t) + + good := validDeleteInput(t) + + tests := []struct { + name string + env cldf.Environment + input func() operations.CREWorkflowDeleteInput + wantErr string + }{ + { + name: "no CRERunner", + env: *envNoCRE, + wantErr: "cre runner is not available in this environment", + }, + { + name: "CRERunner without CLI", + env: *envNoCLI, + wantErr: "CLI runner is not configured", + }, + { + name: "missing project", + env: *envWithCLI, + input: func() operations.CREWorkflowDeleteInput { + in := good + in.Project = creartifacts.ConfigSource{} + + return in + }, + wantErr: "project:", + }, + { + name: "missing deploymentRegistry", + env: *envWithCLI, + input: func() operations.CREWorkflowDeleteInput { + in := good + in.DeploymentRegistry = "" + + return in + }, + wantErr: "deploymentRegistry is required", + }, + { + name: "missing donFamily", + env: *envWithCLI, + input: func() operations.CREWorkflowDeleteInput { + in := good + in.DonFamily = "" + + return in + }, + wantErr: "donFamily is required", + }, + { + name: "missing workflowName", + env: *envWithCLI, + input: func() operations.CREWorkflowDeleteInput { + in := good + in.WorkflowName = "" + + return in + }, + wantErr: "workflowName", + }, + { + name: "valid input passes", + env: *envWithCLI, + }, + } + + cs := CREWorkflowDeleteChangeset{} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + input := good + if tc.input != nil { + input = tc.input() + } + err := cs.VerifyPreconditions(tc.env, input) + if tc.wantErr == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tc.wantErr) + } + }) + } +} + +func TestCREWorkflowDeleteChangeset_Apply(t *testing.T) { + t.Setenv("ONCHAIN_EVM_DEPLOYER_KEY", "abc123") + + cs := CREWorkflowDeleteChangeset{} + input := validDeleteInput(t) + + t.Run("success returns report", func(t *testing.T) { //nolint:paralleltest + mockCLI := cremocks.NewMockCLIRunner(t) + mockCLI.EXPECT().ContextRegistries().Return([]fcre.ContextRegistryEntry{ + {ID: "private", Type: "off-chain"}, + }).Once() + mockCLI.EXPECT(). + Run(mock.Anything, (map[string]string)(nil), matchCLIArgs("workflow", "delete")). + Return(&fcre.CallResult{ + ExitCode: 0, + Stdout: []byte("ok"), + }, nil). + Once() + env := newTestEnv(t, testenv.WithCRERunner(fcre.NewRunner(fcre.WithCLI(mockCLI)))) + + out, err := cs.Apply(*env, input) + require.NoError(t, err) + require.Len(t, out.Reports, 1) + output, ok := out.Reports[0].Output.(operations.CREWorkflowDeleteOutput) + require.True(t, ok) + require.Equal(t, 0, output.ExitCode) + require.Equal(t, "ok", output.Stdout) + }) + + t.Run("operation error returns report and error", func(t *testing.T) { //nolint:paralleltest + mockCLI := cremocks.NewMockCLIRunner(t) + mockCLI.EXPECT().ContextRegistries().Return([]fcre.ContextRegistryEntry{ + {ID: "private", Type: "off-chain"}, + }).Once() + mockCLI.EXPECT().Run(mock.Anything, mock.Anything, mock.Anything). + Return((*fcre.CallResult)(nil), errors.New("op failed")). + Once() + env := newTestEnv(t, testenv.WithCRERunner(fcre.NewRunner(fcre.WithCLI(mockCLI)))) + + out, err := cs.Apply(*env, input) + require.ErrorContains(t, err, "cre workflow delete: op failed") + require.Len(t, out.Reports, 1) + output, ok := out.Reports[0].Output.(operations.CREWorkflowDeleteOutput) + require.True(t, ok) + require.Empty(t, output.Stdout) + }) + + t.Run("on-chain registry injects deployer key env", func(t *testing.T) { //nolint:paralleltest + mockCLI := cremocks.NewMockCLIRunner(t) + mockCLI.EXPECT().ContextRegistries().Return([]fcre.ContextRegistryEntry{ + {ID: "onchain-reg", Type: "on-chain"}, + }).Once() + mockCLI.EXPECT(). + Run(mock.Anything, mock.MatchedBy(func(env map[string]string) bool { + return env != nil && env["CRE_ETH_PRIVATE_KEY"] == "abc123" + }), mock.Anything). + Return(&fcre.CallResult{ExitCode: 0}, nil). + Once() + env := newTestEnv(t, testenv.WithCRERunner(fcre.NewRunner(fcre.WithCLI(mockCLI)))) + + onChainInput := input + onChainInput.DeploymentRegistry = "onchain-reg" + out, err := cs.Apply(*env, onChainInput) + require.NoError(t, err) + require.Len(t, out.Reports, 1) + }) +} diff --git a/cre/operations/workflow_delete.go b/cre/operations/workflow_delete.go new file mode 100644 index 0000000..d4f69ec --- /dev/null +++ b/cre/operations/workflow_delete.go @@ -0,0 +1,198 @@ +package operations + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/Masterminds/semver/v3" + + fcre "github.com/smartcontractkit/chainlink-deployments-framework/cre" + creartifacts "github.com/smartcontractkit/chainlink-deployments-framework/cre/artifacts" + crecli "github.com/smartcontractkit/chainlink-deployments-framework/cre/cli" + fwops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +// CREWorkflowDeleteOutput is the serializable result of a CRE CLI workflow delete invocation. +type CREWorkflowDeleteOutput struct { + ExitCode int `json:"exitCode"` + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` +} + +// CREWorkflowDeleteInput is the resolved input for a CRE workflow delete. +// Project is resolved via [creartifacts.ArtifactsResolver]. Binary and config are not used. +type CREWorkflowDeleteInput struct { + WorkflowName string `json:"workflowName" yaml:"workflowName"` + DonFamily string `json:"donFamily,omitempty" yaml:"donFamily,omitempty"` + DeploymentRegistry string `json:"deploymentRegistry,omitempty" yaml:"deploymentRegistry,omitempty"` + // Project is the path to CRE CLI project.yaml (RPCs, don-family, etc.). + Project creartifacts.ConfigSource `json:"project" yaml:"project"` + // Optional - Context overrides CRE_* process env defaults for the generated context.yaml. + Context crecli.ContextOverrides `json:"context" yaml:"context"` + // Optional - ExtraCREArgs are appended after built-in workflow delete arguments (e.g. org/tenant flags). + ExtraCREArgs []string `json:"extraCreArgs,omitempty" yaml:"extraCreArgs,omitempty"` + // Optional - TargetName is the CRE CLI target key that must match a top-level key + // in project.yaml. Defaults to CREDeployTargetName ("cld-deploy") when empty. + TargetName string `json:"targetName,omitempty" yaml:"targetName,omitempty"` +} + +// Validate trims fields and checks required workflow delete inputs. +func (in *CREWorkflowDeleteInput) Validate() error { + if in == nil { + return errors.New("cre workflow delete input is nil") + } + in.WorkflowName = strings.TrimSpace(in.WorkflowName) + in.DonFamily = strings.TrimSpace(in.DonFamily) + in.DeploymentRegistry = strings.TrimSpace(in.DeploymentRegistry) + if in.WorkflowName == "" { + return errors.New("cre: workflowName is required") + } + + if err := in.Project.Validate(); err != nil { + return fmt.Errorf("project: %w", err) + } + + return nil +} + +func (in CREWorkflowDeleteInput) resolveTargetName() string { + target := strings.TrimSpace(in.TargetName) + if target != "" { + return target + } + + return CREDeployTargetName +} + +// CREWorkflowDeleteOp deletes a workflow via the CRE CLI (single side effect: CLI invocation). +var CREWorkflowDeleteOp = fwops.NewOperation( + "cre-workflow-delete", + semver.MustParse("1.0.0"), + "Deletes a CRE workflow via the CRE CLI subprocess", + func(b fwops.Bundle, deps CREDeployDeps, input CREWorkflowDeleteInput) (CREWorkflowDeleteOutput, error) { + ctx := b.GetContext() + if deps.CLI == nil { + return CREWorkflowDeleteOutput{}, errors.New("cre CLIRunner is nil") + } + + workDir, err := os.MkdirTemp("", "cre-workflow-delete-*") + if err != nil { + return CREWorkflowDeleteOutput{}, fmt.Errorf("mkdir temp workflow delete workspace: %w", err) + } + defer func() { _ = os.RemoveAll(workDir) }() + + resolver, err := creartifacts.NewArtifactsResolver(workDir) + if err != nil { + return CREWorkflowDeleteOutput{}, err + } + + bundleDir := filepath.Join(workDir, creBundleSubdir) + if err = os.MkdirAll(bundleDir, 0o700); err != nil { + return CREWorkflowDeleteOutput{}, err + } + + projectSrc, err := resolver.ResolveConfig(ctx, input.Project) + if err != nil { + return CREWorkflowDeleteOutput{}, fmt.Errorf("resolve project.yaml: %w", err) + } + projectDest := filepath.Join(workDir, "project.yaml") + if err = copyFile(projectSrc, projectDest); err != nil { + return CREWorkflowDeleteOutput{}, fmt.Errorf("copy project.yaml: %w", err) + } + + target := input.resolveTargetName() + workflowCfg := crecli.WorkflowConfig{ + target: { + UserWorkflow: crecli.UserWorkflow{ + DeploymentRegistry: input.DeploymentRegistry, + WorkflowName: input.WorkflowName, + }, + WorkflowArtifacts: crecli.WorkflowArtifacts{ + WorkflowPath: ".", + ConfigPath: "", + SecretsPath: "", + }, + }, + } + workflowYAMLPath, err := crecli.WriteWorkflowYAML(bundleDir, workflowCfg) + if err != nil { + return CREWorkflowDeleteOutput{}, fmt.Errorf("write workflow.yaml: %w", err) + } + + ctxCfg, err := crecli.BuildContextConfig(input.DonFamily, input.Context, deps.CRECfg, deps.CLI.ContextRegistries()) + if err != nil { + return CREWorkflowDeleteOutput{}, err + } + contextPath, err := crecli.WriteContextYAML(workDir, ctxCfg) + if err != nil { + return CREWorkflowDeleteOutput{}, fmt.Errorf("write context.yaml: %w", err) + } + logResolvedFile(b.Logger, "workflow.yaml", workflowYAMLPath, prettyYAML) + logResolvedFile(b.Logger, "project.yaml", projectDest, prettyYAML) + logResolvedFile(b.Logger, "context.yaml", contextPath, prettyYAML) + + envPath, err := crecli.WriteCREEnvFile(workDir, contextPath, deps.CRECfg, input.DonFamily) + if err != nil { + return CREWorkflowDeleteOutput{}, fmt.Errorf("write CRE .env file: %w", err) + } + + args := BuildWorkflowDeleteArgs(target, workDir, envPath, input.ExtraCREArgs) + b.Logger.Infow("Running CRE workflow delete", "args", args) + + var runEnv map[string]string + if crecli.IsOnChainRegistry(input.DeploymentRegistry, crecli.FlatRegistries(ctxCfg)) { + runEnv = map[string]string{ + "CRE_ETH_PRIVATE_KEY": deps.EVMDeployerKey, + } + } + res, runErr := deps.CLI.Run(ctx, runEnv, args...) + if runErr != nil { + var exitErr *fcre.ExitError + if errors.As(runErr, &exitErr) { + return CREWorkflowDeleteOutput{ + ExitCode: exitErr.ExitCode, + Stdout: string(exitErr.Stdout), + Stderr: string(exitErr.Stderr), + }, fmt.Errorf("cre workflow delete: %w", runErr) + } + + return CREWorkflowDeleteOutput{}, fmt.Errorf("cre workflow delete: %w", runErr) + } + if res == nil { + return CREWorkflowDeleteOutput{}, errors.New("cre workflow delete: CLI returned nil result without error") + } + + b.Logger.Infow("CRE workflow delete finished", + "exitCode", res.ExitCode, + "stdout", string(res.Stdout), + "stderr", string(res.Stderr), + ) + + return CREWorkflowDeleteOutput{ + ExitCode: res.ExitCode, + Stdout: string(res.Stdout), + Stderr: string(res.Stderr), + }, nil + }, +) + +// BuildWorkflowDeleteArgs constructs the CRE CLI argument list for `cre workflow delete`. +func BuildWorkflowDeleteArgs(targetName, workDir, envPath string, extra []string) []string { + bundleDir := filepath.Join(workDir, creBundleSubdir) + args := []string{ + "workflow", "delete", + bundleDir, + "-R", workDir, + "-T", targetName, + "--yes", + } + if envPath != "" { + args = append(args, "-e", envPath) + } + args = append(args, extra...) + + return args +} diff --git a/cre/operations/workflow_delete_test.go b/cre/operations/workflow_delete_test.go new file mode 100644 index 0000000..a44cbad --- /dev/null +++ b/cre/operations/workflow_delete_test.go @@ -0,0 +1,222 @@ +package operations + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + fcre "github.com/smartcontractkit/chainlink-deployments-framework/cre" + creartifacts "github.com/smartcontractkit/chainlink-deployments-framework/cre/artifacts" + cremocks "github.com/smartcontractkit/chainlink-deployments-framework/cre/mocks" + cfgenv "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config/env" + fwops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" +) + +func TestCREWorkflowDeleteOp(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input func(t *testing.T) CREWorkflowDeleteInput + setupCLI func(t *testing.T) *cremocks.MockCLIRunner + assert func(t *testing.T, out fwops.Report[CREWorkflowDeleteInput, CREWorkflowDeleteOutput], err error) + }{ + { + name: "success invokes CLI with delete args", + input: func(t *testing.T) CREWorkflowDeleteInput { + t.Helper() + + return CREWorkflowDeleteInput{ + WorkflowName: "wf", + DonFamily: "feeds-zone-a", + DeploymentRegistry: "private", + Project: creartifacts.NewConfigSourceLocal(writeFile(t, "project.yaml", []byte("cld-deploy:\n cre-cli:\n don-family: feeds-zone-a\n"))), + } + }, + setupCLI: func(t *testing.T) *cremocks.MockCLIRunner { + t.Helper() + m := cremocks.NewMockCLIRunner(t) + m.EXPECT().ContextRegistries().Return(testRegistries()).Once() + m.EXPECT().Run(mock.Anything, mock.Anything, matchCLIArgs("workflow", "delete")).Return( + &fcre.CallResult{ExitCode: 0, Stdout: []byte("ok"), Stderr: nil}, nil, + ).Once() + + return m + }, + assert: func(t *testing.T, _ fwops.Report[CREWorkflowDeleteInput, CREWorkflowDeleteOutput], err error) { + t.Helper() + require.NoError(t, err) + }, + }, + { + name: "custom target name is forwarded to CLI", + input: func(t *testing.T) CREWorkflowDeleteInput { + t.Helper() + + return CREWorkflowDeleteInput{ + WorkflowName: "wf", + DonFamily: "feeds-zone-a", + DeploymentRegistry: "private", + Project: creartifacts.NewConfigSourceLocal(writeFile(t, "project.yaml", []byte("production-settings:\n rpcs: []\n"))), + TargetName: "production-settings", + } + }, + setupCLI: func(t *testing.T) *cremocks.MockCLIRunner { + t.Helper() + m := cremocks.NewMockCLIRunner(t) + m.EXPECT().ContextRegistries().Return(testRegistries()).Once() + m.EXPECT().Run(mock.Anything, mock.Anything, mock.MatchedBy(func(args []string) bool { + tIdx := indexOf(args, "-T") + return tIdx >= 0 && tIdx+1 < len(args) && args[tIdx+1] == "production-settings" + })).Return( + &fcre.CallResult{ExitCode: 0, Stdout: []byte("ok"), Stderr: nil}, nil, + ).Once() + + return m + }, + assert: func(t *testing.T, _ fwops.Report[CREWorkflowDeleteInput, CREWorkflowDeleteOutput], err error) { + t.Helper() + require.NoError(t, err) + }, + }, + { + name: "CLI exit error propagates exit code and output", + input: func(t *testing.T) CREWorkflowDeleteInput { + t.Helper() + + return CREWorkflowDeleteInput{ + WorkflowName: "wf", + DonFamily: "feeds-zone-a", + DeploymentRegistry: "private", + Project: creartifacts.NewConfigSourceLocal(writeFile(t, "project.yaml", []byte("cld-deploy: {}\n"))), + } + }, + setupCLI: func(t *testing.T) *cremocks.MockCLIRunner { + t.Helper() + exitErr := &fcre.ExitError{ExitCode: 7, Stdout: []byte("out"), Stderr: []byte("err")} + m := cremocks.NewMockCLIRunner(t) + m.EXPECT().ContextRegistries().Return(testRegistries()).Once() + m.EXPECT().Run(mock.Anything, mock.Anything, mock.Anything).Return( + &fcre.CallResult{ExitCode: 7, Stdout: exitErr.Stdout, Stderr: exitErr.Stderr}, exitErr, + ).Once() + + return m + }, + assert: func(t *testing.T, out fwops.Report[CREWorkflowDeleteInput, CREWorkflowDeleteOutput], err error) { + t.Helper() + require.ErrorContains(t, err, "cre workflow delete") + require.Equal(t, 7, out.Output.ExitCode) + require.Equal(t, "out", out.Output.Stdout) + require.Equal(t, "err", out.Output.Stderr) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + mockCLI := tc.setupCLI(t) + bundle := fwops.NewBundle(func() context.Context { return t.Context() }, logger.Test(t), fwops.NewMemoryReporter()) + deps := CREDeployDeps{ + CLI: mockCLI, + CRECfg: cfgenv.CREConfig{}, + } + + out, err := fwops.ExecuteOperation[CREWorkflowDeleteInput, CREWorkflowDeleteOutput, CREDeployDeps]( + bundle, CREWorkflowDeleteOp, deps, tc.input(t)) + tc.assert(t, out, err) + }) + } +} + +func TestCREWorkflowDeleteInput_resolveTargetName(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input CREWorkflowDeleteInput + expect string + }{ + { + name: "empty defaults to CREDeployTargetName", + input: CREWorkflowDeleteInput{}, + expect: CREDeployTargetName, + }, + { + name: "whitespace defaults to CREDeployTargetName", + input: CREWorkflowDeleteInput{TargetName: " "}, + expect: CREDeployTargetName, + }, + { + name: "custom target is returned trimmed", + input: CREWorkflowDeleteInput{TargetName: " production-settings "}, + expect: "production-settings", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tc.expect, tc.input.resolveTargetName()) + }) + } +} + +func TestBuildWorkflowDeleteArgs(t *testing.T) { + t.Parallel() + + workDir := t.TempDir() + bundleDir := filepath.Join(workDir, creBundleSubdir) + require.NoError(t, os.MkdirAll(bundleDir, 0o700)) + + tests := []struct { + name string + targetName string + envPath string + extra []string + check func(t *testing.T, args []string) + }{ + { + name: "default target with env and extra args", + targetName: CREDeployTargetName, + envPath: filepath.Join(workDir, ".env"), + extra: []string{"--extra"}, + check: func(t *testing.T, args []string) { + t.Helper() + require.Equal(t, []string{ + "workflow", "delete", bundleDir, + "-R", workDir, "-T", CREDeployTargetName, + "--yes", + "-e", filepath.Join(workDir, ".env"), + "--extra", + }, args) + }, + }, + { + name: "custom target without env or extra", + targetName: "production-settings", + check: func(t *testing.T, args []string) { + t.Helper() + require.NotContains(t, args, "-e") + tIdx := indexOf(args, "-T") + require.NotEqual(t, -1, tIdx) + require.Greater(t, len(args), tIdx+1) + require.Equal(t, "production-settings", args[tIdx+1]) + require.Len(t, args, 8) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + tc.check(t, BuildWorkflowDeleteArgs(tc.targetName, workDir, tc.envPath, tc.extra)) + }) + } +}