Skip to content

HanDaber/do

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🚀 Go do Library

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.

✨ Features

  • Programmatic Task Execution: Use do.Runner to 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-task executor 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 tasktask:sub:task)
  • Variable Passing: Pass variables to tasks via KEY=value format with proper CLI variable override support
  • CLI Example: Ready-to-build example in cmd/do/main.go demonstrating the library with embedded Taskfile

🏁 Getting Started

There are two primary approaches to utilizing the do library:

  1. As a Library with Your Own Taskfile: Embed your Taskfile.yaml in your application and use do.Runner for execution.
  2. Using the Pre-built CLI Example: Use the do-cli CLI with the Taskfile.yaml embedded in github.com/d/do.

1. Craft Your Taskfile.yaml

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"}}"

2. Use do.Runner with Your Embedded Taskfile

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:

  • Runner creates a temporary file for the Taskfile content to satisfy go-task executor 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

Task Execution Arguments

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"})

Space-Separated Task Names

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:staging

Error Handling

runner.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
    }
}

Context Control

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)

3. Using the Convenience do.Execute() Function

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:

  1. Uses the Taskfile embedded in the github.com/d/do library itself
  2. Sets up the working directory automatically
  3. Handles argument parsing and task execution

It's ideal for quick CLI tools or when you don't need a custom Taskfile.

Summary: Best Practices

  1. Avoid Task-Level Variables: Never use task-level vars: blocks in embedded Taskfiles; use template defaults or global vars instead
  2. Error Handling: Always check the error returned by runner.Execute() for proper error handling
  3. Context Management: Use context timeouts or cancellation for long-running tasks
  4. Task Naming: Take advantage of space-to-colon translation for more natural CLI commands
  5. Task Orchestration: Use & and && operators for complex task flows

Troubleshooting

  • "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

About

embed a taskfile.yaml and execute its commands from your app

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages