The do library enables embedding and executing Taskfile.yaml (from go-task/task) directly in Go applications. It provides programmatic task execution and CLI integration while solving specific issues with task-level variables in embedded Taskfiles.
- Programmatic Task Execution: Use
do.Runnerto run tasks from Go code with full control over I/O and execution flow - Embedded Taskfile Support: Properly handles embedded Taskfile content with temporary file creation to satisfy
go-taskexecutor requirements - Task Orchestration: Sequential execution (
&&) and concurrent execution (&) with proper error handling and output management - Space-to-Colon Translation: Converts space-separated task names to colon-separated format (e.g.,
task sub task→task:sub:task) - Variable Passing: Pass variables to tasks via
KEY=valueformat with proper CLI variable override support - CLI Example: Ready-to-build example in
cmd/do/main.godemonstrating the library with embedded Taskfile
There are two primary approaches to utilizing the do library:
- As a Library with Your Own Taskfile: Embed your
Taskfile.yamlin your application and usedo.Runnerfor execution. - Using the Pre-built CLI Example: Use the
do-cliCLI with theTaskfile.yamlembedded ingithub.com/d/do.
CRITICAL: When embedding a Taskfile, avoid task-level vars: blocks as they cause two issues:
- Inconsistent task loading during
Executor.Setup() - CLI variable override failures when using
task.Call.Vars
Instead, use template defaults or global vars: blocks.
Example Taskfile.yaml:
version: '3'
# Global vars are OK (task-level vars cause problems with embedded Taskfiles)
vars:
GLOBAL_VAR: global_default
tasks:
hello:
desc: "Prints a hello message"
cmds:
- echo "Hello from embedded Taskfile!"
silent: true
greet:
# DON'T use task-level vars blocks like this:
# vars:
# USER: default_user # ⚠️ Will cause problems with CLI overrides
# DO use template defaults instead:
desc: "Greets a user. Usage: do-cli greet USER=YourName"
cmds:
- 'echo "Greetings, {{.USER | default "User"}}!"'
env-check:
desc: "Uses both global and passed variables"
cmds:
- echo "GLOBAL_VAR={{.GLOBAL_VAR}}, CUSTOM_VAR={{.CUSTOM_VAR | default "not_set"}}"The Runner struct handles all the complexities of embedding a Taskfile, including creating temporary files to satisfy the go-task executor requirements.
package main
import (
"context"
_ "embed"
"fmt"
"os"
"time"
"github.com/d/do"
)
//go:embed MyTaskfile.yaml
var taskfileContent []byte
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
// Initialize runner with embedded Taskfile content
runner := &do.Runner{
TaskfileContent: taskfileContent,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
}
// Execute task(s)
if err := runner.Execute(ctx, os.Args[1:]); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}Implementation Details:
Runnercreates a temporary file for the Taskfile content to satisfygo-taskexecutor requirements- It properly configures the executor with the correct working directory and entrypoint
- The AST's Location field is set to the temporary Taskfile path to prevent "no such file" errors
- Temporary files are automatically cleaned up after execution
The runner.Execute(ctx, args) method accepts arguments that define which tasks to run, with what variables, and in what order:
// Single task
runner.Execute(ctx, []string{"my-task"})
// Task with variables
runner.Execute(ctx, []string{"my-task", "VAR1=value1", "VAR2=value2"})
// Sequential execution (taskA, then taskB)
runner.Execute(ctx, []string{"taskA", "&&", "taskB"})
// Concurrent execution (taskA and taskB simultaneously)
runner.Execute(ctx, []string{"taskA", "&", "taskB"})
// Complex orchestration (taskA and taskB concurrently, then taskC with a variable)
runner.Execute(ctx, []string{"taskA", "&", "taskB", "&&", "taskC", "API_KEY=secret"})The library automatically translates space-separated task names to colon-separated format:
// This:
runner.Execute(ctx, []string{"deploy staging"})
// Is equivalent to this in standard go-task:
// task deploy:stagingrunner.Execute() returns an error for various failure scenarios:
if err := runner.Execute(ctx, args); err != nil {
switch {
case strings.Contains(err.Error(), "task not found"):
// Handle unknown task
case strings.Contains(err.Error(), "exit status"):
// Handle command failure
case strings.Contains(err.Error(), "concurrent tasks failed"):
// Handle failure in concurrent task group
default:
// Handle other errors
}
}Use context for timeouts, cancellation, and deadline control:
// With timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// With cancellation
ctx, cancel := context.WithCancel(context.Background())
go func() {
// Cancel on some condition
time.Sleep(5 * time.Second)
cancel()
}()
runner.Execute(ctx, args)For simple cases, the library provides a convenience function that uses the embedded Taskfile from the library itself:
package main
import (
"fmt"
"os"
"github.com/d/do"
)
func main() {
if err := do.Execute(os.Args[1:]); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}This function:
- Uses the Taskfile embedded in the
github.com/d/dolibrary itself - Sets up the working directory automatically
- Handles argument parsing and task execution
It's ideal for quick CLI tools or when you don't need a custom Taskfile.
- Avoid Task-Level Variables: Never use task-level
vars:blocks in embedded Taskfiles; use template defaults or global vars instead - Error Handling: Always check the error returned by
runner.Execute()for proper error handling - Context Management: Use context timeouts or cancellation for long-running tasks
- Task Naming: Take advantage of space-to-colon translation for more natural CLI commands
- Task Orchestration: Use
&and&&operators for complex task flows
- "Task X does not exist" errors: Check for task-level
vars:blocks in your Taskfile and remove them - Variable overrides not working: Ensure you're using template defaults (
{{.VAR | default "value"}}) instead of task-level vars - File not found errors: The library handles temporary file creation, but ensure your tasks reference files relative to the working directory