Development guide
This guide outlines how to build, test, and develop with the DataRobot CLI.
GitHub CLI Recommendation: DataRobot recommends using the GitHub CLI (
gh) for fork management. All examples useghcommands. See the GitHub CLI installation guide if needed. If you prefer manual git workflow, you can replaceghcommands with equivalentgitoperations.
Table of contents
- Build from source
- Directory structure
- Coding standards
- Development workflow
- Testing
- Debugging
- Release process
Build from source
Prerequisites
- Go 1.26.4+
- Git version control
- Task (The task runner)
Quick build
# Fork and clone the repository
gh repo fork datarobot-oss/cli --clone
cd cli
# Install development tools
task dev-init
# Build binary
task build
# Binary is at ./dist/dr
./dist/dr self version
Available tasks
# Show all tasks
task --list
# Common tasks
task build # Build the CLI binary
task test # Run all tests
task test-coverage # Run tests with coverage
task lint # Run linters (includes formatting)
task clean # Clean build artifacts
task dev-init # Setup development environment
task install-tools # Install development tools
task run # Run CLI without building
Build options
Always use task build for building the CLI to ensure that the proper version information and build flags are applied.
# Standard build (recommended)
task build
# Run without building (for quick testing)
task run -- templates list
The task build command automatically includes:
- Version information from Git
- The Git commit hash
- A build timestamp
- Proper
ldflagsconfiguration
For cross-platform builds and releases, task build includes GoReleaser (see Release Process).
Directory structure
cli/
├── cmd/ # Command implementations (Cobra)
│ ├── root.go # Root command and global flags
│ ├── auth/ # Authentication commands
│ │ ├── cmd.go # Auth command group
│ │ ├── login.go # Login command
│ │ ├── logout.go # Logout command
│ │ └── setURL.go # Set URL command
│ ├── dotenv/ # Environment variable management
│ │ ├── cmd.go # Dotenv command
│ │ ├── model.go # TUI model (Bubble Tea)
│ │ ├── promptModel.go # Prompt handling
│ │ ├── template.go # Template parsing
│ │ └── variables.go # Variable handling
│ ├── run/ # Task execution
│ │ └── cmd.go # Run command
│ ├── templates/ # Template management
│ │ ├── cmd.go # Template command group
│ │ ├── clone/ # Clone subcommand
│ │ ├── list/ # List subcommand
│ │ ├── setup/ # Setup wizard
│ │ └── status.go # Status command
│ └── self/ # CLI utility commands
│ ├── cmd.go # Self command group
│ ├── completion.go # Completion generation
│ └── version.go # Version command
├── internal/ # Private packages (not importable)
│ ├── assets/ # Embedded assets
│ │ └── templates/ # HTML templates
│ ├── config/ # Configuration management
│ │ ├── config.go # Config loading/saving
│ │ ├── auth.go # Auth config
│ │ └── constants.go # Constants
│ ├── drapi/ # DataRobot API client
│ │ ├── llmGateway.go # LLM Gateway API
│ │ └── templates.go # Templates API
│ ├── envbuilder/ # Environment configuration
│ │ ├── builder.go # Env file building
│ │ └── discovery.go # Prompt discovery
│ ├── task/ # Task runner integration
│ │ ├── discovery.go # Taskfile discovery
│ │ └── runner.go # Task execution
│ └── version/ # Version information
│ └── version.go
├── tui/ # Terminal UI shared components
│ ├── banner.go # ASCII banner
│ └── theme.go # Color theme
├── docs/ # Documentation
├── main.go # Application entry point
├── go.mod # Go module dependencies
├── go.sum # Dependency checksums
├── Taskfile.yaml # Task definitions
└── goreleaser.yaml # Release configuration
Key components
Command layer (cmd/)
The CLI is built using the Cobra framework.
Commands are hierarchically organized. There should be a one-to-one mapping between commands and files/directories. For example, the templates command group is in cmd/templates/, with subcommands in their own directories.
Code in the cmd/ folder should primarily handle command-line parsing, argument validation, and the orchestration of calls to internal packages. There should be minimal to no business logic here. Consider this the UI layer of the application.
// cmd/root.go - Root command definition
var RootCmd = &cobra.Command{
Use: "dr",
Short: "DataRobot CLI",
Long: "Command-line interface for DataRobot",
}
// Register subcommands
RootCmd.AddCommand(
auth.Cmd(),
templates.Cmd(),
// ...
)
TUI layer
The TUI layer contains the cmd/dotenv/ and cmd/templates/setup/ directories. It uses Bubble Tea for interactive UIs.
// Bubble Tea model
type Model struct {
// State
screen screens
// Sub-models
textInput textinput.Model
list list.Model
}
// Required methods
func (m Model) Init() tea.Cmd
func (m Model) Update(tea.Msg) (tea.Model, tea.Cmd)
func (m Model) View() string
Internal packages
Internal packages (internal/) house core business logic, API clients, configuration management, and more.
Configuration
Configuration is found in internal/config/. The CLI uses Viper for configuration as well as a state registry.
// Load config
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("~/.datarobot")
viper.ReadInConfig()
// Access values
endpoint := viper.GetString("datarobot.endpoint")
API client
Using the API client directory, internal/drapi/, you can make requests to the HTTP client for DataRobot APIs.
// Make API request
func GetTemplates() (*TemplateList, error) {
resp, err := http.Get(endpoint + "/api/v2/templates")
// ... handle response
}
Design patterns
Command pattern
Each command is self-contained:
// cmd/templates/list/cmd.go
var Cmd = &cobra.Command{
Use: "list",
Short: "List templates",
GroupID: "core",
RunE: func(cmd *cobra.Command, args []string) error {
// Implementation
return listTemplates()
},
}
RunE is the main execution function. Cobra also provides PreRunE, PostRunE, and other hooks. DataRobot recommends using these functions for setup, teardown, and validation.
PersistPreRunE: func(cmd *cobra.Command, args []string) error {
// Setup logging
return setupLogging()
},
PreRunE: func(cmd *cobra.Command, args []string) error {
// Validate args
return validateArgs(args)
},
PostRunE: func(cmd *cobra.Command, args []string) error {
// Cleanup
return nil
},
Each command can be assigned to a group via GroupID for better organization in dr help views. Commands without a GroupID are listed under Additional commands.
Model-view-update
Interactive UIs like Bubble Tea use the Model-view-update (MVU) pattern:
// Update handles events
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
return m.handleKey(msg)
case dataLoadedMsg:
return m.handleData(msg)
}
return m, nil
}
// View renders current state
func (m Model) View() string {
return lipgloss.JoinVertical(
lipgloss.Left,
m.header(),
m.content(),
m.footer(),
)
}
Coding standards
Go style requirements
Critical: All code must pass golangci-lint with zero errors. Strictly follow these whitespace rules:
- Never cuddle declarations Always add a blank line before
var,const, andtypedeclarations when they follow other statements. - Separate statement types: Add blank lines between different statement types (assign, if, for, return, etc.).
- Blank line after block start: Add a blank line after the opening braces of functions/blocks when they follow declarations.
- Blank line before multi-line statements: Add a blank line before if/for/switch statements.
Review an example of correct spacing below.
func example() {
x := 1
if x > 0 {
y := 2
fmt.Println(y)
}
var result string
result = "done"
return result
}
The example below outlines common mistakes to avoid.
// ❌ BAD: Cuddled declaration
func bad() {
x := 1
var y int // Missing a blank line before the declaration
}
// ✅ GOOD: Properly spaced
func good() {
x := 1
var y int
}
TUI development standards
Consider the following when building terminal user interfaces.
- Always use the
tui.Runwrapper to execute TUI models. This ensures globalCtrl-Chandling and sets up logging to.dr-tui-debug.log.
-
Reuse existing TUI components. Check
tui/package first before creating new components. Also explore the Bubbles library for pre-built components. -
Use common lipgloss styles. The styles are defined in tui/styles.go for visual consistency:
import "github.com/datarobot/cli/tui"
// Use theme styles
title := tui.TitleStyle.Render("My Title")
error := tui.ErrorStyle.Render("Error message")
Quality tools
All code must pass these tools without errors:
go mod tidy: Dependency managementgo vet: Suspicious constructsgolangci-lint run: Comprehensive linting (includes wsl, revive, staticcheck, etc.)golangci-lint fmt: Comprehensive formatting via gofumptgoreleaser check: Release configuration validation
Before committing code, verify it follows wsl (whitespace) rules.
Run quality checks
# Run all quality checks at once
task lint
# Individual checks
go mod tidy
go vet ./...
task install-tools # Install golangci-lint
./tmp/bin/golangci-lint fmt ./...
./tmp/bin/golangci-lint run ./...
./tmp/bin/goreleaser check
Updating golangci-lint for Go version upgrades
When upgrading the Go version in go.mod, you may need to update golangci-lint to ensure compatibility:
- Check the golangci-lint releases for a version that supports your target Go version
- Update
GOLANGCI_LINT_VERSIONinTaskfile.yamlto the new version - Run
task install-toolsto download the pre-built binary for the new version - Run
task lintto verify all checks pass
Important: golangci-lint is installed as a standalone pre-built binary (via the install script in Taskfile.yaml), not via go install. This means:
- Version mismatches between your project's Go version and golangci-lint's internal Go version are handled automatically
- The pre-built binary includes its own Go runtime, so it works with any project Go version
- Always use
task install-toolsafter updating the version variable
Development workflow
Always use Taskfile tasks for development operations and not direct go commands. This ensures consistency, proper build flags, and a correct environment setup.
# ✅ CORRECT: Use task commands
task build
task test
task lint
# ❌ INCORRECT: Don't use direct go commands
go build
go test
1. Set up the development environment
# Fork and clone the repository
gh repo fork datarobot-oss/cli --clone
cd cli
# Setup development environment
task dev-init
2. Create a feature branch
3. Make changes
4. Test changes
# Run tests
task test
# Run a specific test (direct go test is acceptable for specific tests)
go test -run TestMyFeature ./cmd/templates
# Test manually using task run
task run -- templates list
# Or build and test the binary
task build
./dist/dr templates list
5. Commit and push
Testing
Unit tests
Unit tests are *_test.go files co-located with the code they test.
// cmd/auth/login_test.go
package auth
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLogin(t *testing.T) {
// Arrange
mockAPI := &MockAPI{}
// Act
err := performLogin(mockAPI)
// Assert
assert.NoError(t, err)
}
Integration tests
Integration tests are also written as Go tests (*_test.go). They typically exercise interactions between multiple packages and/or use temporary on-disk state.
// internal/config/config_test.go
func TestConfigReadWrite(t *testing.T) {
// Create temp config
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "config.yaml")
// Write config
err := SaveConfig(configPath, &Config{
Endpoint: "https://test.datarobot.com",
})
assert.NoError(t, err)
// Read config
config, err := LoadConfig(configPath)
assert.NoError(t, err)
assert.Equal(t, "https://test.datarobot.com", config.Endpoint)
}
TUI tests
Using teatest:
// cmd/dotenv/model_test.go
func TestDotenvModel(t *testing.T) {
m := Model{
// Setup model
}
tm := teatest.NewTestModel(t, m)
// Send keypress
tm.Send(tea.KeyMsg{Type: tea.KeyEnter})
// Wait for update
teatest.WaitFor(t, tm.Output(), func(bts []byte) bool {
return bytes.Contains(bts, []byte("Expected output"))
})
}
Running tests
# All tests (recommended)
task test
# With coverage (opens an HTML report)
task test-coverage
# Specific package (direct go test is fine for targeted testing)
go test ./internal/config
# Verbose
go test -v ./...
# With race detection (task test already includes this)
go test -race ./...
# Specific test
go test -run TestLogin ./cmd/auth
Note: task test automatically runs tests with race detection and coverage enabled.
Go version requirements for race detection
The -race flag requires the race runtime library to match your Go compiler version exactly. If you see an error like:
This means your installed Go version doesn't match the version specified in go.mod. Go's GOTOOLCHAIN=auto setting (the default) automatically downloads the required toolchain, but the race runtime comes from your local GOROOT installation.
To resolve:
- Upgrade Go to match
go.mod:brew upgrade go(macOS) - Or downgrade
go.mod:go mod edit -go=1.X.Z(where1.X.Zis your installed version) - Or force the downloaded toolchain:
export GOTOOLCHAIN=go1.X.Y(where1.X.Yis the version ingo.mod)
Run smoke tests
Smoke tests verify the CLI works end-to-end with a real DataRobot environment.
Run locally:
# Set your DataRobot API token
export DR_API_TOKEN=your-token
# Run smoke tests
task smoke-test
# Windows
task smoke-test-windows
Run via GitHub Actions:
Smoke tests are not automatically run on Pull Requests. You can trigger them using PR comments:
/trigger-smoke-testor/trigger-test-smoke: Run smoke tests on a PR./trigger-install-testor/trigger-test-install: Run installation tests on a PR.
Daily automated smoke tests also run in CI.
Windows shell detection tests:
The Windows smoke tests include shell detection verification. To test shell detection independently:
# From PowerShell (should detect "powershell")
.\smoke_test_scripts\windows\run_shell_detection_test_powershell.ps1
# From cmd.exe (should detect "cmd")
.\smoke_test_scripts\windows\run_shell_detection_test_cmd.bat
These tests verify that the CLI correctly identifies the shell from which it's invoked by inspecting the parent process using tasklist.
Debugging
Use Delve
# Install delve
go install github.com/go-delve/delve/cmd/dlv@latest
# Debug with arguments
dlv debug main.go -- templates list
# In debugger
(dlv) break main.main
(dlv) continue
(dlv) print variableName
(dlv) next
Debug logging
See the user guide for how --verbose and --debug flags work and where log files are written.
Adding debug output
import (
"github.com/datarobot/cli/internal/log"
)
// Debug logging
log.Debug("Variable value", "key", value)
log.Info("Processing started")
log.Warn("Unexpected condition")
log.Error("Operation failed", "error", err)
[!WARNING] Never log user-provided input (including prompt responses), and avoid logging secrets (tokens, passwords, etc.).
Release process
See Releasing documentation for a detailed overview of the release process.
Quick release
# Tag version
git tag v1.0.0
git push upstream --tags
# GitHub Actions will:
# 1. Build for all platforms
# 2. Run tests
# 3. Create GitHub release
# 4. Upload binaries
See also
- Contributing guide
- Project structure—code organization and design
- Release process—how releases are created and published