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.5+
- 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 dupcheck # Check for duplicate code (jscpd)
task precommit # Run pre-commit checks (format, tidy, vet, lint)
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.
Git hooks (lefthook)
Lefthook enforces quality checks before each commit. It is a Go binary installed
by task install-tools and wired up by task dev-init (which runs lefthook install).
Hooks run automatically on git commit and reuse Taskfile tasks (task precommit,
task dupcheck) so checks are always consistent between local commits and CI.
Run manually with task precommit. Bypass with LEFTHOOK=0 git commit.
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