Skip to content
Closed
2 changes: 2 additions & 0 deletions cmd/pipeline/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/datarobot/cli/cmd/pipeline/graph"
"github.com/datarobot/cli/cmd/pipeline/list"
"github.com/datarobot/cli/cmd/pipeline/lock"
"github.com/datarobot/cli/cmd/pipeline/run"
"github.com/datarobot/cli/cmd/pipeline/update"
"github.com/datarobot/cli/cmd/pipeline/version"
"github.com/datarobot/cli/internal/features"
Expand Down Expand Up @@ -51,6 +52,7 @@ input payloads, runs, and recurring schedules.`,
lock.Cmd(),
version.Cmd(),
graph.Cmd(),
run.Cmd(),
)

return cmd
Expand Down
1 change: 1 addition & 0 deletions cmd/pipeline/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func TestCmd_HasExpectedSubcommands(t *testing.T) {
"lock": false,
"version": false,
"graph": false,
"run": false,
}

for _, sub := range cmd.Commands() {
Expand Down
79 changes: 79 additions & 0 deletions cmd/pipeline/run/cancel/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2026 DataRobot, Inc. and its affiliates.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cancel

import (
"errors"
"fmt"

"github.com/datarobot/cli/cmd/pipeline/scopeflag"
"github.com/datarobot/cli/internal/auth"
"github.com/datarobot/cli/internal/pipeline"
"github.com/datarobot/cli/internal/telemetry"
"github.com/datarobot/cli/tui"
"github.com/spf13/cobra"
)

func Cmd() *cobra.Command {
var flags scopeflag.Flags

cmd := &cobra.Command{
Use: "cancel <run-id>",
Short: "Cancel a pipeline run",
Long: `Request cancellation of an in-flight run.

The API rejects cancellation if the run has already reached a terminal
state (COMPLETED, FAILED, CANCELLED).

Example:
dr pipeline run cancel --pipeline <id> <run-id>
dr pipeline run cancel --pipeline <id> --version=2 <run-id>`,
Args: cobra.ExactArgs(1),
PreRunE: auth.EnsureAuthenticatedE,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if flags.PipelineID == "" {
return errors.New("--pipeline is required")
}

scope, version, err := flags.Resolve(cmd)
if err != nil {
return err
}

err = pipeline.CancelRun(flags.PipelineID, scope, version, args[0])
if err != nil {
return err
}

fmt.Println(tui.BaseTextStyle.Render("Cancelled run: " + args[0]))

return nil
},
}

flags.Bind(cmd)

telemetry.TrackWith(cmd, func(_ *cobra.Command, args []string) map[string]any {
return map[string]any{
"pipeline_id": flags.PipelineID,
"run_id": telemetry.FirstArg(args),
"scope": flags.Scope,
"version": flags.Version,
}
})

return cmd
}
56 changes: 56 additions & 0 deletions cmd/pipeline/run/cancel/cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2026 DataRobot, Inc. and its affiliates.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cancel

import (
"io"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func runCmd(t *testing.T, args ...string) error {
t.Helper()

cmd := Cmd()
cmd.SetArgs(args)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.PreRunE = nil

return cmd.Execute()
}

func TestCmd_RejectsMissingPipeline(t *testing.T) {
err := runCmd(t, "d-1")
require.Error(t, err)
assert.Contains(t, err.Error(), "--pipeline")
}

func TestCmd_RejectsBadScopeCombo(t *testing.T) {
err := runCmd(t, "--pipeline", "p", "--scope", "locked", "d-1")
require.Error(t, err)
assert.Contains(t, err.Error(), "requires --version")
}

func TestCmd_RequiresPositional(t *testing.T) {
err := runCmd(t, "--pipeline", "p")
require.Error(t, err)
}

func TestCmd_Name(t *testing.T) {
assert.Equal(t, "cancel", Cmd().Name())
}
49 changes: 49 additions & 0 deletions cmd/pipeline/run/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2026 DataRobot, Inc. and its affiliates.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package run

import (
"github.com/datarobot/cli/cmd/pipeline/run/cancel"
"github.com/datarobot/cli/cmd/pipeline/run/create"
"github.com/datarobot/cli/cmd/pipeline/run/get"
"github.com/datarobot/cli/cmd/pipeline/run/list"
"github.com/datarobot/cli/cmd/pipeline/run/status"
"github.com/spf13/cobra"
)

// Cmd returns the parent command for `dr pipeline run`.
func Cmd() *cobra.Command {
cmd := &cobra.Command{
Use: "run",
Short: "Manage pipeline runs",
Long: `Trigger and inspect runs (single executions) of a pipeline.

Runs come in two scopes:
- draft : executes against the in-flight draft of a pipeline
- locked : executes against a specific frozen version

When --version is supplied, the locked scope is selected automatically.`,
}

cmd.AddCommand(
create.Cmd(),
list.Cmd(),
get.Cmd(),
status.Cmd(),
cancel.Cmd(),
)

return cmd
}
41 changes: 41 additions & 0 deletions cmd/pipeline/run/cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2026 DataRobot, Inc. and its affiliates.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package run

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestCmd_RegistersAllVerbs(t *testing.T) {
cmd := Cmd()

want := map[string]bool{
"create": false,
"list": false,
"get": false,
"status": false,
"cancel": false,
}

for _, sub := range cmd.Commands() {
want[sub.Name()] = true
}

for verb, present := range want {
assert.Truef(t, present, "missing subcommand: %s", verb)
}
}
77 changes: 77 additions & 0 deletions cmd/pipeline/run/create/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2026 DataRobot, Inc. and its affiliates.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package create

import (
"github.com/datarobot/cli/cmd/pipeline/scopeflag"
"github.com/datarobot/cli/internal/auth"
"github.com/datarobot/cli/internal/pipeline"
"github.com/datarobot/cli/internal/telemetry"
"github.com/spf13/cobra"
)

func Cmd() *cobra.Command {
var (
flags scopeflag.Flags
inputID string
outputFormat pipeline.OutputFormat
)

cmd := &cobra.Command{
Use: "create",
Short: "Trigger a pipeline run",
Long: `Trigger a new run (single execution) of a pipeline.

The run is created in PENDING state. Use ` + "`dr pipeline run get`" + `
or ` + "`dr pipeline run status`" + ` to follow its progress.

Example:
dr pipeline run create --pipeline <id> --input <input-id>
dr pipeline run create --pipeline <id> --version=2 --input <input-id> --output-format json`,
Args: cobra.NoArgs,
PreRunE: auth.EnsureAuthenticatedE,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, _ []string) error {
scope, version, err := flags.Resolve(cmd)
if err != nil {
return err
}

result, err := pipeline.CreateRun(flags.PipelineID, scope, version, inputID)
if err != nil {
return err
}

return pipeline.RenderRun(outputFormat, *result)
},
}

flags.Bind(cmd)
_ = cmd.MarkFlagRequired("pipeline")
cmd.Flags().StringVar(&inputID, "input", "", "Input ID to trigger the run with")
_ = cmd.MarkFlagRequired("input")
pipeline.AddOutputFlag(cmd, &outputFormat)

telemetry.TrackWith(cmd, func(_ *cobra.Command, _ []string) map[string]any {
return map[string]any{
"pipeline_id": flags.PipelineID,
"scope": flags.Scope,
"version": flags.Version,
"output_format": string(outputFormat),
}
})

return cmd
}
67 changes: 67 additions & 0 deletions cmd/pipeline/run/create/cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2026 DataRobot, Inc. and its affiliates.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package create

import (
"io"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func runCmd(t *testing.T, args ...string) error {
t.Helper()

cmd := Cmd()
cmd.SetArgs(args)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.PreRunE = nil

return cmd.Execute()
}

func TestCmd_RejectsInvalidOutput(t *testing.T) {
err := runCmd(t, "--pipeline", "p", "--input", "in-1", "--output-format", "yaml")
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid output format")
}

func TestCmd_RejectsMissingPipeline(t *testing.T) {
err := runCmd(t, "--input", "in-1")
require.Error(t, err)
assert.Contains(t, err.Error(), "pipeline")
}

func TestCmd_RejectsMissingInput(t *testing.T) {
err := runCmd(t, "--pipeline", "p")
require.Error(t, err)
assert.Contains(t, err.Error(), "input")
}

func TestCmd_RejectsBadScopeCombo(t *testing.T) {
err := runCmd(t, "--pipeline", "p", "--input", "in-1", "--scope", "draft", "--version", "2")
require.Error(t, err)
assert.Contains(t, err.Error(), "draft cannot be combined")
}

func TestCmd_HasExpectedFlags(t *testing.T) {
cmd := Cmd()

for _, name := range []string{"pipeline", "scope", "version", "input", "output-format"} {
assert.NotNilf(t, cmd.Flags().Lookup(name), "expected --%s flag", name)
}
}
Loading