This document outlines recommended improvements for the Rot task runner, organized by category and priority.
- Feature Improvements
- Code Quality Improvements
- Testing & Reliability
- Developer Experience
- Performance Optimizations
- Security Enhancements
Add configurable timeouts to prevent runaway tasks from hanging indefinitely.
// TaskDefinition.cs
public int? Timeout { get; set; } // Timeout in secondstasks:
long-running:
command: npm run build
timeout: 300 # 5 minutesAllow running multiple tasks by group or pattern.
rot run --group build # Run all tasks in "build" group
rot run --pattern "test-*" # Run all tasks matching pattern// TaskDefinition.cs
public string Group { get; set; } = string.Empty;
public string[] Tags { get; set; } = Array.Empty<string>();Re-run tasks automatically when files change.
rot watch build --glob "src/**/*.cs"
rot watch test --glob "**/*.cs" --debounce 500Preview what would be executed without actually running commands.
rot run build --dry-run
# Output: Would execute: dotnet build
# Working directory: /project
# Environment: DEBUG=trueCapture and store task output for later inspection or piping.
rot run build --output build.log
rot run build --json # Output results as JSONRun tasks based on conditions (file existence, environment, OS).
tasks:
windows-build:
command: msbuild
condition:
os: windows
deploy:
command: ./deploy.sh
condition:
env: CI=true
fileExists: dist/bundle.jsExecute commands before and after tasks.
tasks:
build:
command: dotnet build
preTasks: [clean]
postTasks: [notify]Support variables and interpolation in commands.
variables:
outputDir: ./dist
config: Release
tasks:
build:
command: dotnet build -c ${config} -o ${outputDir}Prompt for input during task execution.
tasks:
deploy:
command: ./deploy.sh ${environment}
prompts:
environment:
message: "Deploy to which environment?"
choices: [staging, production]Create shortcuts for common task combinations.
aliases:
ci: [clean, build, test, pack]
dev: [restore, build, watch]rot ci # Runs clean, build, test, pack in sequenceExecute tasks on remote machines via SSH.
tasks:
deploy-prod:
command: systemctl restart app
remote:
host: prod-server.example.com
user: deployFine-grained control over parallel execution.
tasks:
test-all:
dependsOn: [test-unit, test-integration, test-e2e]
parallel: 2 # Run max 2 dependencies at onceCache task results based on input file hashes.
tasks:
build:
command: dotnet build
cache:
inputs: ["**/*.cs", "*.csproj"]
outputs: ["bin/", "obj/"]Define different configurations for different environments.
profiles:
dev:
env:
DEBUG: "true"
prod:
env:
OPTIMIZE: "true"rot run build --profile prodAllow extending Rot with custom task types.
plugins:
- rot-plugin-docker
- rot-plugin-kubernetes
tasks:
deploy:
type: docker
image: myapp:latestSplit TaskExecutor.cs (252 lines) into focused classes:
Services/
├── TaskExecutor.cs # Core execution orchestration
├── TaskLoader.cs # Configuration loading (JSON/YAML)
├── ProcessRunner.cs # Process execution logic
├── ConsoleFormatter.cs # Colored output formatting
└── DependencyResolver.cs # Dependency graph resolution
Replace Console.WriteLine with a proper logging abstraction.
public class TaskExecutor
{
private readonly ILogger<TaskExecutor> _logger;
// Use log levels: Debug, Info, Warning, Error
_logger.LogInformation("Executing task {TaskName}", taskName);
_logger.LogDebug("Process started with PID {ProcessId}", process.Id);
}Support log output destinations:
- Console (default)
- File
- Structured JSON
Add validation for task configurations with helpful error messages.
public class TaskValidator
{
public ValidationResult Validate(TaskDefinition task)
{
var errors = new List<string>();
if (string.IsNullOrEmpty(task.Command))
errors.Add("Task 'command' is required");
if (task.Type != "shell" && task.Type != "process")
errors.Add($"Invalid task type '{task.Type}'. Use 'shell' or 'process'");
if (task.Timeout.HasValue && task.Timeout.Value <= 0)
errors.Add("Timeout must be a positive number");
return new ValidationResult(errors);
}
}Provide actionable error messages with context.
Current: "Task 'build' not found."
Improved: "Task 'build' not found in tasks.yaml.
Available tasks: clean, test, deploy
Did you mean 'rebuild'?"
Support graceful shutdown with CancellationToken.
public async Task<int> ExecuteTaskAsync(string taskName, CancellationToken ct = default)
{
// ...
await process.WaitForExitAsync(ct);
// Handle Ctrl+C gracefully
}Replace exit codes with a proper result type.
public record TaskResult(
bool Success,
int ExitCode,
string TaskName,
TimeSpan Duration,
string? ErrorMessage = null
);
public async Task<TaskResult> ExecuteTaskAsync(string taskName)
{
// Return rich result instead of just exit code
}Document public APIs for IntelliSense support.
/// <summary>
/// Executes a task and all its dependencies.
/// </summary>
/// <param name="taskName">The name of the task to execute.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
/// <exception cref="TaskNotFoundException">Task does not exist.</exception>
public async Task<int> ExecuteTaskAsync(string taskName)Properly dispose of resources.
public class TaskExecutor : IDisposable
{
private readonly SemaphoreSlim _executionSemaphore = new(1, 1);
private bool _disposed;
public void Dispose()
{
if (!_disposed)
{
_executionSemaphore.Dispose();
_disposed = true;
}
}
}Create comprehensive unit tests for core functionality.
Rot.Tests/
├── TaskExecutorTests.cs
├── TaskLoaderTests.cs
├── TaskValidatorTests.cs
├── DependencyResolverTests.cs
└── Fixtures/
├── valid-tasks.json
├── invalid-tasks.json
└── circular-deps.yaml
Test Coverage Goals:
- TaskExecutor: Dependency resolution, circular detection, execution order
- TaskLoader: JSON parsing, YAML parsing, error handling
- CLI: Argument parsing, command routing
Test actual command execution in isolated environments.
[Fact]
public async Task ExecuteTask_RunsShellCommand_ReturnsExitCode()
{
var tempDir = CreateTempDirectory();
var tasksFile = CreateTasksFile(tempDir, new { build = new { command = "echo hello" } });
var executor = TaskExecutor.LoadFromFile(tasksFile);
var result = await executor.ExecuteTaskAsync("build");
Assert.Equal(0, result);
}Update GitHub Actions to actually run tests.
# ci.yml
- name: Run tests
run: dotnet test --configuration Release --logger "trx" --results-directory TestResults
- name: Upload test results
uses: actions/upload-artifact@v3
with:
name: test-results
path: TestResults/Measure performance for large task graphs.
[Benchmark]
public async Task ExecuteTaskWithManyDependencies()
{
await _executor.ExecuteTaskAsync("root-with-50-deps");
}Use Stryker.NET to ensure test quality.
dotnet strykerShow more information about tasks.
$ rot list
Available tasks:
build Build the project [shell]
test Run tests [shell] depends: build
deploy Deploy to production [shell] depends: build, test
Groups: build (2), test (1), deploy (1)Show detailed information about a specific task.
$ rot describe build
Task: build
Label: Build the project
Type: shell
Command: dotnet build
Cwd: ./src
Env:
- DOTNET_CLI_TELEMETRY_OPTOUT=1
Dependencies: clean, restore
Dependents: test, deployGenerate completions for bash, zsh, fish, PowerShell.
rot completion bash > /etc/bash_completion.d/rot
rot completion zsh > ~/.zsh/completions/_rotControl output verbosity.
rot run build --verbose # Show detailed execution info
rot run build --quiet # Only show errors
rot run build -q # Short formShow task dependency graph.
$ rot graph
build
├── clean
└── restore
test
└── build
├── clean
└── restore
deploy
└── test
└── build
├── clean
└── restoreSupport different project types in rot init.
rot init --template dotnet
rot init --template node
rot init --template python
rot init --template dockerSearch up directory tree for tasks file.
public static string? FindTasksFile(string startDir)
{
var dir = startDir;
while (dir != null)
{
var yamlPath = Path.Combine(dir, "tasks.yaml");
var jsonPath = Path.Combine(dir, "tasks.json");
if (File.Exists(yamlPath)) return yamlPath;
if (File.Exists(jsonPath)) return jsonPath;
dir = Path.GetDirectoryName(dir);
}
return null;
}Only parse tasks that are actually needed.
private readonly Lazy<Dictionary<string, TaskDefinition>> _tasks;Cache results of completed tasks within a session.
private readonly Dictionary<string, int> _completedTasks = new();
public async Task<int> ExecuteTaskAsync(string taskName)
{
if (_completedTasks.TryGetValue(taskName, out var cachedResult))
return cachedResult;
// ...
}Validate all tasks concurrently at load time.
var validationTasks = _tasks.Select(t => Task.Run(() => Validate(t)));
var results = await Task.WhenAll(validationTasks);Use StringBuilder for large argument lists.
private string BuildArguments(string[] args)
{
if (args.Length == 0) return string.Empty;
if (args.Length == 1) return args[0];
var sb = new StringBuilder(args.Sum(a => a.Length + 1));
foreach (var arg in args)
{
if (sb.Length > 0) sb.Append(' ');
sb.Append(arg);
}
return sb.ToString();
}Prevent shell injection via environment variables.
public static bool IsValidEnvValue(string value)
{
// Reject values with shell metacharacters in unsafe contexts
var dangerous = new[] { ';', '|', '&', '$', '`', '\n', '\r' };
return !dangerous.Any(c => value.Contains(c));
}Validate commands before execution.
public static bool IsSafeCommand(string command)
{
// Warn or block dangerous patterns
var dangerous = new[] { "rm -rf /", ":(){ :|:& };:", "dd if=" };
return !dangerous.Any(d => command.Contains(d, StringComparison.OrdinalIgnoreCase));
}Ensure working directories are within expected paths.
public static bool IsValidWorkingDirectory(string cwd, string projectRoot)
{
var fullPath = Path.GetFullPath(cwd);
return fullPath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase);
}Support secure environment variable sources.
tasks:
deploy:
command: ./deploy.sh
env:
API_KEY: ${secret:api-key} # Load from secure store
DB_PASS: ${env:DB_PASSWORD} # Load from parent envLog task executions for compliance.
public class AuditLogger
{
public void LogExecution(string taskName, string command, DateTime timestamp, int exitCode)
{
// Write to audit log file
}
}- ✅ Task timeout support
- ✅ Dry run mode
- ✅ Unit tests
- ✅ Structured logging
- ✅ Configuration validation
- ✅ Task groups and batch execution (
--group,--pattern,--tagoptions) - ✅ Variable substitution (
${var}and${env:VAR}syntax) - ✅ Watch mode (
rot watchcommand with file monitoring) - ✅
--verbose/--quietflags - ✅ Better
listoutput (groups, types, dependencies) - ✅
describecommand for detailed task information
- ✅ Conditional execution (OS, env, fileExists, fileNotExists conditions)
- ✅ Task hooks (preTasks and postTasks)
- ✅ Profile support (
--profileflag with variables and env overrides) - ✅ Task caching (based on input file hashes with
--no-cachebypass) - ✅ Plugin system (ITaskTypeProvider interface and PluginLoader)
- ✅ Task Aliases - Create shortcuts for common task combinations (
aliasesin config) - ✅ Shell Completions - Generate shell completions (
rot completion bash|zsh|fish|powershell) - ✅ Config File Auto-Discovery - Search up directory tree for tasks file
- ✅ Task Graph Visualization - Show task dependency graph (
rot graph [task]) - ✅ Result Pattern for Error Handling -
TaskResultandTasksResulttypes withExecuteTaskWithResultAsync
- ✅ Task Output Capture - Capture and store task output (
--output file.log,--jsonflags) - ✅ Init Templates - Project-specific templates (
rot init --template dotnet|node|python|docker) - ✅ Security Enhancements - Environment variable sanitization, command validation, working directory validation
- ✅ Integration Tests - Comprehensive tests for actual command execution in isolated environments
- ✅ Improved Error Messages - Actionable error messages with context and suggestions