mirror of
https://github.com/go-task/task.git
synced 2026-06-15 03:41:37 +00:00
In scoped mode, CLI vars (e.g., `task foo VAR=value`) now correctly
override task-level vars. This is achieved by:
1. Adding a `CLIVars` field to the Compiler struct
2. Storing CLI globals in this field after parsing
3. Applying CLI vars last in scoped mode to ensure they override everything
The order of variable resolution in scoped mode is now:
1. OS env → {{.env.XXX}}
2. Root taskfile env → {{.env.XXX}}
3. Root taskfile vars → {{.VAR}}
4. Include taskfile env/vars (if applicable)
5. IncludeVars (vars passed via includes: section)
6. Task-level vars
7. CLI vars (highest priority)
Legacy mode behavior is unchanged.
206 lines
4.7 KiB
Go
206 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
|
|
"github.com/spf13/pflag"
|
|
|
|
"github.com/go-task/task/v3"
|
|
"github.com/go-task/task/v3/args"
|
|
"github.com/go-task/task/v3/errors"
|
|
"github.com/go-task/task/v3/experiments"
|
|
"github.com/go-task/task/v3/internal/filepathext"
|
|
"github.com/go-task/task/v3/internal/flags"
|
|
"github.com/go-task/task/v3/internal/logger"
|
|
"github.com/go-task/task/v3/internal/version"
|
|
"github.com/go-task/task/v3/taskfile/ast"
|
|
)
|
|
|
|
func main() {
|
|
if err := run(); err != nil {
|
|
l := &logger.Logger{
|
|
Stdout: os.Stdout,
|
|
Stderr: os.Stderr,
|
|
Verbose: flags.Verbose,
|
|
Color: flags.Color,
|
|
}
|
|
if err, ok := err.(*errors.TaskRunError); ok && flags.ExitCode {
|
|
emitCIErrorAnnotation(err)
|
|
l.Errf(logger.Red, "%v\n", err)
|
|
os.Exit(err.TaskExitCode())
|
|
}
|
|
if err, ok := err.(errors.TaskError); ok {
|
|
emitCIErrorAnnotation(err)
|
|
l.Errf(logger.Red, "%v\n", err)
|
|
os.Exit(err.Code())
|
|
}
|
|
emitCIErrorAnnotation(err)
|
|
l.Errf(logger.Red, "%v\n", err)
|
|
os.Exit(errors.CodeUnknown)
|
|
}
|
|
os.Exit(errors.CodeOk)
|
|
}
|
|
|
|
// emitCIErrorAnnotation emits an error annotation for supported CI providers.
|
|
func emitCIErrorAnnotation(err error) {
|
|
if isGA, _ := strconv.ParseBool(os.Getenv("GITHUB_ACTIONS")); !isGA {
|
|
return
|
|
}
|
|
if e, ok := err.(*errors.TaskRunError); ok {
|
|
fmt.Fprintf(os.Stdout, "::error title=Task '%s' failed::%v\n", e.TaskName, e.Err)
|
|
return
|
|
}
|
|
fmt.Fprintf(os.Stdout, "::error title=Task failed::%v\n", err)
|
|
}
|
|
|
|
func run() error {
|
|
log := &logger.Logger{
|
|
Stdout: os.Stdout,
|
|
Stderr: os.Stderr,
|
|
Verbose: flags.Verbose,
|
|
Color: flags.Color,
|
|
}
|
|
|
|
if err := flags.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := experiments.Validate(); err != nil {
|
|
log.Warnf("%s\n", err.Error())
|
|
}
|
|
|
|
if flags.Version {
|
|
fmt.Println(version.GetVersionWithBuildInfo())
|
|
return nil
|
|
}
|
|
|
|
if flags.Help {
|
|
pflag.Usage()
|
|
return nil
|
|
}
|
|
|
|
if flags.Experiments {
|
|
return log.PrintExperiments()
|
|
}
|
|
|
|
if flags.Init {
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
args, _, err := args.Get()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := wd
|
|
if len(args) > 0 {
|
|
name := args[0]
|
|
if filepathext.IsExtOnly(name) {
|
|
name = filepathext.SmartJoin(filepath.Dir(name), "Taskfile"+filepath.Ext(name))
|
|
}
|
|
path = filepathext.SmartJoin(wd, name)
|
|
}
|
|
finalPath, err := task.InitTaskfile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !flags.Silent {
|
|
if flags.Verbose {
|
|
log.Outf(logger.Default, "%s\n", task.DefaultTaskfile)
|
|
}
|
|
log.Outf(logger.Green, "Taskfile created: %s\n", filepathext.TryAbsToRel(finalPath))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if flags.Completion != "" {
|
|
script, err := task.Completion(flags.Completion)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(script)
|
|
return nil
|
|
}
|
|
|
|
e := task.NewExecutor(
|
|
flags.WithFlags(),
|
|
task.WithVersionCheck(true),
|
|
)
|
|
if err := e.Setup(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if flags.ClearCache {
|
|
cachePath := filepath.Join(e.TempDir.Remote, "remote")
|
|
return os.RemoveAll(cachePath)
|
|
}
|
|
|
|
listOptions := task.NewListOptions(
|
|
flags.List,
|
|
flags.ListAll,
|
|
flags.ListJson,
|
|
flags.NoStatus,
|
|
flags.Nested,
|
|
)
|
|
if listOptions.ShouldListTasks() {
|
|
if flags.Silent {
|
|
return e.ListTaskNames(flags.ListAll)
|
|
}
|
|
foundTasks, err := e.ListTasks(listOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !foundTasks {
|
|
os.Exit(errors.CodeUnknown)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Parse the remaining arguments
|
|
cliArgsPreDash, cliArgsPostDash, err := args.Get()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
calls, globals := args.Parse(cliArgsPreDash...)
|
|
|
|
// If there are no calls, run the default task instead
|
|
if len(calls) == 0 {
|
|
calls = append(calls, &task.Call{Task: "default"})
|
|
}
|
|
|
|
// Merge CLI variables first (e.g. FOO=bar) so they take priority over Taskfile defaults
|
|
e.Taskfile.Vars.Merge(globals, nil)
|
|
// Store CLI vars for scoped mode where they need highest priority
|
|
e.Compiler.CLIVars = globals
|
|
|
|
// Then ReverseMerge special variables so they're available for templating
|
|
cliArgsPostDashQuoted, err := args.ToQuotedString(cliArgsPostDash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
specialVars := ast.NewVars()
|
|
specialVars.Set("CLI_ARGS", ast.Var{Value: cliArgsPostDashQuoted})
|
|
specialVars.Set("CLI_ARGS_LIST", ast.Var{Value: cliArgsPostDash})
|
|
specialVars.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
|
|
specialVars.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
|
|
specialVars.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
|
|
specialVars.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline})
|
|
specialVars.Set("CLI_ASSUME_YES", ast.Var{Value: flags.AssumeYes})
|
|
e.Taskfile.Vars.ReverseMerge(specialVars, nil)
|
|
if !flags.Watch {
|
|
e.InterceptInterruptSignals()
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
if flags.Status {
|
|
return e.Status(ctx, calls...)
|
|
}
|
|
|
|
return e.Run(ctx, calls...)
|
|
}
|