Testdrive mirrors your GitHub Actions run: steps locally so you can catch failures before pushing. It discovers workflows, respects job and step filters, and outputs either a concise pretty report or machine-friendly JSON.
go install github.com/bgricker/testdrive/cmd/testdrive@latest# List the jobs and steps that would run
$ testdrive list
# Execute all run: steps sequentially
$ testdrive run
# Preview commands without executing
$ testdrive run --dry-run
# Filter by job/steps and switch formats
$ testdrive run --job test --only-step "Lint" --format json
# Stream command output as it runs
$ testdrive run --verbose
# Use local environment only, ignoring workflow env variables
$ testdrive run --use-local-env
# Skip specific steps (e.g., database setup when you already have a working DB)
$ testdrive run --skip-step "Set up database"
# Transform lint commands to auto-fix mode
$ testdrive run --auto-fix
# Allow privileged commands (e.g., sudo/apt-get) when absolutely necessary
$ TESTDRIVE_ALLOW_PRIVILEGED=1 testdrive run
# Watch for file changes and auto-rerun workflows
$ testdrive watch
# Watch with smart filtering - only run tests related to changed files
$ testdrive watch --smart-filterWhen format is pretty (default) and not in verbose mode, Testdrive renders a live, GitHub-style summary:
- ✅/❌ per job with individual timers
- 🟢 while a job is running, ⏳ when queued
- Failed jobs expand to show step breakdown, durations, the exact
Command:run, and cleaned failure output (including parsed RSpec failures) - Routine CI noise is suppressed in streaming mode to keep output focused
Example:
✅ lint (3.2s)
✅ scan_js (1.9s)
❌ test (31.4s)
⏭️ Install packages (0s)
✅ Install modules (247ms)
❌ Run tests (31.1s)
Command: bin/rails db:setup spec
spec/jobs/foo_spec.rb:123 expected X got Y
Flags such as --workflow, --job, --only-step, and --skip-step accept multiple values and support substring or /regex/ matches. When no workflows are provided, Testdrive automatically loads .github/workflows/*.yml/*.yaml in lexicographic order. Execution stops with a non-zero exit code if any step fails, but all remaining steps continue to run so you see the full picture.
Testdrive automatically inherits your shell environment and supports version managers:
- asdf: Automatically sources
asdf.sh(orasdf.fishfor fish shell) to ensure correct Ruby, Node, Python versions - rbenv: Works with your existing rbenv setup
- Shell compatibility: Supports bash, zsh, ksh, sh, and fish shells
- Environment variables: Merges workflow → job → step environment variables (override with
--use-local-env) - Working directories: Respects
working-directorysettings from workflows
Testdrive intelligently handles environment variables to make local testing seamless:
Smart Auto-Detection:
When your workflow defines services: (like PostgreSQL, Redis, etc.), testdrive automatically uses your local environment instead of workflow env variables. This happens because testdrive can't run services containers, so the workflow's env vars (pointing to non-existent services) won't work anyway.
# If your workflow has services:, local env is used automatically!
$ testdrive run
# Warning shown: "services are not supported; automatically using local environment instead"Manual Override: You can explicitly control environment behavior:
# Force local env (even without services)
$ testdrive run --use-local-env
# Or configure it permanently in .testdrive.yml:
use_local_env: trueThis is particularly useful when:
- Your workflow defines CI-specific database connections (like
postgres:postgres@localhost) - You have local credentials in
.envfiles or shell environment - You want consistent behavior across all workflows
When developing locally, you often want linters to automatically fix issues rather than just report them. Use --auto-fix to transform lint commands to their auto-fix variants:
# Automatically transforms common linting tools to fix mode
$ testdrive run --auto-fixBuilt-in transformations:
| Original Command | Transformed Command |
|---|---|
rubocop --parallel |
rubocop -A |
bin/rails standard |
bin/rails standard:fix |
standard |
standard --fix |
standardrb |
standardrb --fix |
prettier --check |
prettier --write |
eslint |
eslint --fix |
ruff check |
ruff check --fix |
black --check |
black |
Custom transformations can be configured in .testdrive.yml:
auto_fix: true # Enable by default
auto_fix_rules:
- pattern: 'yarn lint'
replace: 'yarn fix:prettier'
- pattern: 'custom-linter'
remove_flags: ['--strict']
add_flags: ['--fix']Watch mode automatically re-runs your workflows when files change, providing immediate feedback during development:
# Watch for changes and auto-rerun workflows
$ testdrive watch
# Watch mode respects all the same filters and options
$ testdrive watch --job test --auto-fixFeatures:
- Debounced re-runs: Multiple rapid file changes are batched together (default 300ms)
- Smart ignore patterns: Automatically ignores common paths like
node_modules,vendor,.git,tmp,log, and more - Clear terminal: Optionally clears the screen between runs for a clean view (enabled by default)
- Graceful shutdown: Press Ctrl+C to stop watching
Configuration in .testdrive.yml:
watch:
debounce_ms: 300 # Wait time before re-running (default: 300ms)
clear_on_run: true # Clear terminal between runs (default: true)
ignore_patterns: # Additional patterns to ignore (extends defaults)
- "**/*.tmp"
- "**/build/**"
include_patterns: # Only watch these patterns (optional)
- "**/*.go"
- "**/*.yaml"Default ignore patterns include:
**/node_modules/****/vendor/****/tmp/****/log/****/.git/****/.testdrive/****/*.log**/.DS_Store**/coverage/**
Smart filtering dramatically speeds up watch mode for large codebases by only running tests related to changed files. When enabled, testdrive intelligently maps source files to their corresponding test files.
# Enable smart filtering in watch mode
$ testdrive watch --smart-filter
# Or set it as default in config
$ cat .testdrive.yml
smart_filter: trueHow it works:
When files change, testdrive:
- If a test file changed → runs that test
- If a source file changed → finds and runs related tests using smart mapping rules
Built-in mapping rules:
| Source File | Related Tests |
|---|---|
app/models/user.rb |
spec/models/user_spec.rb |
app/controllers/users_controller.rb |
spec/controllers/users_controller_spec.rbspec/requests/*_spec.rbspec/integration/*_spec.rb |
src/components/Button.tsx |
**/*Button*.test.tsx**/*Button*.spec.tsx |
lib/utils.py |
tests/test_utils.py |
pkg/server/handler.go |
pkg/server/handler_test.go |
Custom mapping rules in .testdrive.yml:
smart_filter: true
smart_filter_rules:
# Map API files to both unit and integration tests
- pattern: "app/api/**/*.rb"
test_pattern: "spec/api/**/*_spec.rb"
additional:
- "spec/requests/api/**/*_spec.rb"
# Map React components to their test files
- pattern: "src/components/**/*.tsx"
test_pattern: "src/components/**/*.test.tsx"
additional:
- "src/__tests__/components/**/*.test.tsx"
# Map Go packages to their tests
- pattern: "internal/**/*.go"
test_pattern: "internal/**/*_test.go"Supported test frameworks:
- RSpec (Rails) - automatically modifies
bundle exec rspeccommands - Go - automatically modifies
go testcommands - Jest/npm test - automatically modifies JavaScript test commands
- pytest - automatically modifies Python test commands
An optional .testdrive.yml can provide defaults for the CLI. Command-line flags always win over config values.
provider: github # auto|github (defaults to auto)
workflows:
- .github/workflows/ci.yml
jobs:
- test
only_step:
- /lint/
skip_step:
- "Upload artifact"
- "Set up database schema" # Skip DB setup if you already have a working database
use_local_env: false # Set to true to ignore workflow env variables
auto_fix: false # Set to true to transform lint commands to auto-fix mode
auto_fix_rules: # Custom auto-fix transformations (replaces defaults if provided)
- pattern: 'yarn lint'
replace: 'yarn fix:prettier'
watch: # Watch mode configuration
debounce_ms: 300
clear_on_run: true
ignore_patterns:
- "**/*.tmp"
include_patterns: # Optional: only watch specific file types
- "**/*.go"
smart_filter: false # Set to true to enable smart filtering by default
smart_filter_rules: # Custom file-to-test mappings (replaces defaults if provided)
- pattern: "app/api/**/*.rb"
test_pattern: "spec/api/**/*_spec.rb"
additional:
- "spec/requests/api/**/*_spec.rb"
dry_run: false
verbose: false
format: pretty # pretty|json
warn:
version_mismatch: true # warn when local Ruby/Node major.minor differs
privileged_command_patterns:
- (?i)^sudo\b
- (?i)\bapt-get\b- ✅ GitHub Actions workflow parser (run steps only)
- ✅ Sequential execution with env/shell/working-directory resolution
- ✅ Pretty & JSON reporters; streaming GitHub-style UI with live timers
- ✅ Dry-run, verbose streaming, job/step filters, repeatable
--workflow - ✅ Environment inheritance with asdf/rbenv support
- ✅ Cross-shell compatibility (bash, zsh, ksh, sh, fish)
- ✅ Privileged command detection and skipping
- ✅ Watch mode with file change detection and auto-rerun
- ✅ Auto-fix mode for transforming linters to fix mode
- ✅ Smart file filtering - only run tests related to changed files
- 🚧 Upcoming: parallel job execution, interactive TUI, git hook integration
- Version mismatch warnings are enabled by default; set
warn.version_mismatch: falseto silence them.
- Version mismatch warnings are enabled by default; set
Want to dig in? Run go test ./... to exercise the parser, runner, and CLI tests.