Skip to content

Latest commit

 

History

106 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

taskrun

Go mod version Actions Status GoDoc GitHub tag (latest SemVer) Go Report Card License

github.com/gookit/taskrun is an embeddable Go task and script runner. A Go 1.23+ application can load a task definition, inspect the plan, run tasks and script files, and get an isolated, cancelable, classified result without initializing a CLI framework or any global state.

中文说明

Quick start

go run ./cmd/taskrun -config ./examples/basic.json -task hello
go run ./cmd/taskrun -config ./examples/basic.json -task check -dry-run
package main

import (
	"context"
	"log"
	"os"

	"github.com/gookit/taskrun"
)

func main() {
	dir, err := os.Getwd()
	if err != nil {
		log.Fatal(err)
	}
	runner, err := taskrun.New(taskrun.Definition{
		Version: 1,
		BaseDir: dir,
		Tasks: map[string]taskrun.Task{
			"check": {
				Steps: []taskrun.Step{
					{Exec: &taskrun.ExecSpec{Program: "go", Args: []string{"version"}}},
				},
			},
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	result, err := runner.Run(context.Background(), taskrun.Request{
		Task: "check",
		IO:   taskrun.IO{Stdout: os.Stdout, Stderr: os.Stderr},
	})
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("status=%s", result.Status)
}

BaseDir must be absolute. New copies and freezes the definition, so later mutations by the caller have no effect, and one Runner may serve concurrent runs. log.Fatal above is consumer behavior: the library never exits the process.

Definition schema (version 1)

version: 1
vars:
  target: ./...
env:
  KS_ROOT: "."
env_paths: ["/opt/ks-bin"]
tasks:
  check:
    desc: check the project
    deps: [test]
    timeout: 2s
    steps:
      - exec:
          program: go
          args: [vet, "${vars.target}"]
  test:
    steps:
      - shell:
          name: sh
          script: 'printf "%s\n" "$KS_LABEL"'
        env:
          KS_LABEL: "${vars.target}"
  generate:
    steps:
      - file:
          name: generator
          args: [--check]
  notify:
    dynamic_vars:
      revision:
        exec: {program: git, args: [rev-parse, --short, HEAD]}
    steps:
      - host:
          name: app.notify
          args: ["${vars.revision}"]
files:
  generator:
    path: scripts/generate.go
    interpreter:
      program: go
      prefix_args: [run]

YAML, JSON and TOML decode into the same model. Unknown fields, duplicate keys, type errors, path escapes, mixed step actions, missing references and negative timeouts are rejected with the source file and field path, before anything runs.

timeout is a duration string (500ms, 2s). Task fields: desc, if, platform, deps, dir, timeout, clean_env, env, env_paths, vars, dynamic_vars, steps. Step fields: name, if, platform, dir, timeout, ignore_error, env, env_paths, vars, dynamic_vars and exactly one of exec, shell, file, task, host.

Actions

Action Meaning
exec executable plus argv; no shell parsing, no implicit word splitting
shell explicit shell (sh, bash, zsh, cmd, pwsh, powershell) with its own argument contract
file named entry from files, run as interpreter.program + prefix_args + absolute script path
task serial call of another task; args replaces the inherited arguments, forward_args appends the request arguments
host handler registered with WithHandler, immutable after New

Variables, environment and directories

Lowest to highest precedence:

Setting Order
Vars definition defaults → task → step → Request.Vars
Env WithBaseEnv snapshot → definition → task → step → Request.Env
PATH EnvPaths step → task → definition, prepended to the effective PATH
Dir Request.Dir or BaseDir, then task dir, then step dir; absolute paths stay absolute

clean_env: true on a task drops only the base environment snapshot; explicit env values remain. The library never calls os.Chdir, os.Setenv, never re-reads the process environment during a run and never writes results to disk.

Levels resolve from the bottom up, so a definition level default is rendered before Request.Vars is merged and therefore cannot reference a request variable: ${vars.target}/repo in the definition vars fails with taskrun: invalid_definition: unknown variable "target". Compose runtime paths at the task or step level instead, where request variables are visible:

vars:
  skill: hello            # definition defaults: literal values only
tasks:
  install:
    vars:
      repo_dir: "${vars.target}/repo"   # task level: request vars are visible

Templates are single-pass and namespaced: ${vars.name}, ${env.NAME}, ${args.N} (1-based), ${host.name}, ${run.task}, ${run.dir}, ${run.call}, ${run.os}, ${run.arch}. Unknown references are errors and $${ escapes a literal ${. Dynamic variables declared in dynamic_vars are resolved at most once per task call or step, before first use, through the same engine, context and output limit as other actions.

Conditions use expr and must evaluate to a bool. They may read vars, env, args, host and run, plus bare variable names such as enabled. Task conditions are evaluated before dependencies, step conditions after the step's dynamic variables and before the action. Inspect never runs a dynamic variable command: it reports those fields as deferred.

Execution, cancellation and output

  • Dependencies and task calls run serially, once per occurrence; there is no implicit de-duplication and no implicit retry.
  • Static cycles and over-deep call chains are rejected by New with the full path, before any action runs. A run also enforces a call expansion budget.
  • The effective deadline is the earliest of the parent context and the task or step timeout. Cancellation stops scheduling new steps; Result.Status becomes canceled or timed_out and the returned RunError preserves context.Canceled / context.DeadlineExceeded.
  • The default engine owns a process tree: POSIX children run in their own process group, Windows children in a Job Object. A canceled tree is asked to stop, then killed after the grace period. When the owning mechanism cannot be established (for example a restricted job object already owns the process on a CI runner), the engine falls back to terminating the tree through live parent process ids, which still kills descendants rather than only the parent. Set WithEngine(ProcessEngine{TreeKill: TreeKillRequired}) to fail the action instead of falling back.
  • IO.CaptureLimit bounds the collected bytes per stream. With a writer set, output is both forwarded and captured; after the bound, collection stops and output is marked truncated but keeps draining. A writer error terminates the action and is reported as an IO error. CaptureLimit 0 streams without collecting; negative values are rejected.
  • ignore_error tolerates only a started process with a non-zero exit code or a handler business error. It never tolerates a start failure, cancellation, timeout, output-limit, IO or configuration error.

Observing execution

runner, err := taskrun.New(def, taskrun.WithObserver(func(event taskrun.Event) {
	log.Printf("%s task=%s step=%s depth=%d status=%s reason=%s err=%v",
		event.Kind, event.Task, event.Step, event.Depth, event.Status, event.Reason, event.Err)
}))

Events cover the run (run_started, run_finished), task calls (task_started, task_skipped, task_finished) and steps (step_started, step_skipped, step_finished), and carry the call id, action kind, effective directory, exit code and the classified error. A task reports task_started only when it really runs, so a skipped task reports just task_skipped with a reason. Observers cannot change scheduling and do not return errors; callbacks of one run arrive in order, while concurrent runs may call the observer concurrently, so the host should synchronize and return quickly. Inspect and DryRun emit no events, and events never carry captured output: read Result for that.

Statuses and errors

Result.Status is one of succeeded, succeeded_with_warnings, failed, canceled, timed_out, skipped or dry_run; it never contradicts the returned error. Result.Tasks records every task call, including skipped calls and their reason, and Result.Steps records kind, start state, exit code, output, truncation and error per step.

Failures return *taskrun.RunError with Kind, task, call id, step and source, and support errors.Is/errors.As against ErrNotFound, ErrInvalidRequest, ErrInvalidDefinition, ErrDependencyCycle, ErrExpansionLimit, ErrStart, ErrExit, ErrHandler, ErrOutputLimit, ErrIO, context.Canceled and context.DeadlineExceeded. ErrNotFound means only that the requested root task name is unknown; a missing dependency is an invalid definition, and a load failure is reported by the loader.

Inspect(ctx, req) and Request{DryRun: true} validate arguments and expand the call graph without running any action, handler or dynamic variable command.

Loading and discovery

def, err := formats.LoadFile("tasks.yaml")           // baseDir becomes absolute
files, err := formats.Discover(formats.DiscoverOptions{
	Mode:     formats.Ancestors,                     // or formats.Nearest
	Names:    []string{"tasks", ".kite.task"},
	StartDir: dir,
	StopDir:  root,
	MaxDepth: 8,
})
def, err = formats.LoadFiles(files, false)           // false: conflicts are errors

Discovery is always explicit: names, start directory, stop directory and depth come from the caller, and nothing above the described levels is read. nearest stops at the first level with a match; ancestors collects one file per level from the root down. Merging errors on duplicate task or script file names unless override is enabled, in which case the later definition replaces the whole task; vars and env are overridden by key and every source is recorded in Definition.Sources.

formats.LegacyMap converts the historical Kite task map shape for the Kite adapter; see docs/kite-migration.md. Command aliases, extensions, plugins and system command fallback stay in Kite.

Platforms and versions

Go 1.23 or newer, MIT licensed. Supported runtime platforms are Windows, Linux and macOS; shell selection is explicit and shell sources are not portable across platforms by assumption. The module depends on expr-lang/expr, goccy/go-yaml and BurntSushi/toml only.

Development

make check        # gofmt check, build, vet and tests
make test-go123   # run the tests with the declared minimum Go version
make test-race    # needs cgo and a C compiler
make cross        # linux and darwin builds
make cli          # run the example CLI against examples/basic.json
make examples     # run the Go examples

About

An embeddable Go task and script runner.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages