mirror of
https://github.com/go-task/task.git
synced 2026-04-24 19:54:16 +02:00
542fe465e9
Adds a new `gitlab` output style that wraps each task's output in GitLab CI collapsible section markers. Section IDs are generated automatically so that start and end markers always match and stay unique per invocation — even when the same task runs multiple times in one job. Options: `collapsed` (maps to GitLab's native `[collapsed=true]`) and `error_only` (Task-level behavior, identical to `group.error_only`). Also introduces `output-ci-auto` (taskrc + TASK_OUTPUT_CI_AUTO env var) that auto-selects a CI-aware output style when a supported CI runner is detected (currently `GITLAB_CI=true` → gitlab) and no output style is explicitly configured. Keeps the Taskfile neutral so local devs are not forced into CI-shaped output. Refs #2806.
333 lines
7.9 KiB
Go
333 lines
7.9 KiB
Go
package task
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/sajari/fuzzy"
|
|
|
|
"github.com/go-task/task/v3/errors"
|
|
"github.com/go-task/task/v3/internal/env"
|
|
"github.com/go-task/task/v3/internal/execext"
|
|
"github.com/go-task/task/v3/internal/filepathext"
|
|
"github.com/go-task/task/v3/internal/logger"
|
|
"github.com/go-task/task/v3/internal/output"
|
|
"github.com/go-task/task/v3/internal/version"
|
|
"github.com/go-task/task/v3/taskfile"
|
|
"github.com/go-task/task/v3/taskfile/ast"
|
|
)
|
|
|
|
func (e *Executor) Setup() error {
|
|
e.setupLogger()
|
|
node, err := e.getRootNode()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := e.setupTempDir(); err != nil {
|
|
return err
|
|
}
|
|
if err := e.readTaskfile(node); err != nil {
|
|
return err
|
|
}
|
|
e.setupStdFiles()
|
|
if err := e.setupOutput(); err != nil {
|
|
return err
|
|
}
|
|
if err := e.setupCompiler(); err != nil {
|
|
return err
|
|
}
|
|
if err := e.readDotEnvFiles(); err != nil {
|
|
return err
|
|
}
|
|
if err := e.doVersionChecks(); err != nil {
|
|
return err
|
|
}
|
|
e.setupDefaults()
|
|
e.setupConcurrencyState()
|
|
return nil
|
|
}
|
|
|
|
func (e *Executor) getRootNode() (taskfile.Node, error) {
|
|
node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout,
|
|
taskfile.WithCACert(e.CACert),
|
|
taskfile.WithCert(e.Cert),
|
|
taskfile.WithCertKey(e.CertKey),
|
|
)
|
|
var taskNotFoundError errors.TaskfileNotFoundError
|
|
if errors.As(err, &taskNotFoundError) {
|
|
taskNotFoundError.AskInit = true
|
|
return nil, taskNotFoundError
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e.Dir = node.Dir()
|
|
e.Entrypoint = node.Location()
|
|
return node, err
|
|
}
|
|
|
|
func (e *Executor) readTaskfile(node taskfile.Node) error {
|
|
ctx, cf := context.WithTimeout(context.Background(), e.Timeout)
|
|
defer cf()
|
|
debugFunc := func(s string) {
|
|
e.Logger.VerboseOutf(logger.Magenta, s)
|
|
}
|
|
promptFunc := func(s string) error {
|
|
return e.Logger.Prompt(logger.Yellow, s, "n", "y", "yes")
|
|
}
|
|
reader := taskfile.NewReader(
|
|
taskfile.WithInsecure(e.Insecure),
|
|
taskfile.WithDownload(e.Download),
|
|
taskfile.WithOffline(e.Offline),
|
|
taskfile.WithTrustedHosts(e.TrustedHosts),
|
|
taskfile.WithTempDir(e.TempDir.Remote),
|
|
taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration),
|
|
taskfile.WithReaderCACert(e.CACert),
|
|
taskfile.WithReaderCert(e.Cert),
|
|
taskfile.WithReaderCertKey(e.CertKey),
|
|
taskfile.WithDebugFunc(debugFunc),
|
|
taskfile.WithPromptFunc(promptFunc),
|
|
)
|
|
graph, err := reader.Read(ctx, node)
|
|
if err != nil {
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
return &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: e.Timeout}
|
|
}
|
|
return err
|
|
}
|
|
if e.Taskfile, err = graph.Merge(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *Executor) setupFuzzyModel() {
|
|
if e.Taskfile == nil {
|
|
return
|
|
}
|
|
|
|
model := fuzzy.NewModel()
|
|
model.SetThreshold(1) // because we want to build grammar based on every task name
|
|
|
|
var words []string
|
|
for name, task := range e.Taskfile.Tasks.All(nil) {
|
|
if task.Internal {
|
|
continue
|
|
}
|
|
words = append(words, name)
|
|
words = slices.Concat(words, task.Aliases)
|
|
}
|
|
|
|
model.Train(words)
|
|
e.fuzzyModel = model
|
|
}
|
|
|
|
func (e *Executor) setupTempDir() error {
|
|
if e.TempDir != (TempDir{}) {
|
|
return nil
|
|
}
|
|
|
|
tempDir := env.GetTaskEnv("TEMP_DIR")
|
|
if tempDir == "" {
|
|
e.TempDir = TempDir{
|
|
Remote: filepathext.SmartJoin(e.Dir, ".task"),
|
|
Fingerprint: filepathext.SmartJoin(e.Dir, ".task"),
|
|
}
|
|
} else if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") {
|
|
tempDir, err := execext.ExpandLiteral(tempDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
projectDir, _ := filepath.Abs(e.Dir)
|
|
projectName := filepath.Base(projectDir)
|
|
e.TempDir = TempDir{
|
|
Remote: tempDir,
|
|
Fingerprint: filepathext.SmartJoin(tempDir, projectName),
|
|
}
|
|
|
|
} else {
|
|
e.TempDir = TempDir{
|
|
Remote: filepathext.SmartJoin(e.Dir, tempDir),
|
|
Fingerprint: filepathext.SmartJoin(e.Dir, tempDir),
|
|
}
|
|
}
|
|
|
|
// RemoteCacheDir from taskrc/env can override the remote cache directory
|
|
if e.RemoteCacheDir != "" {
|
|
if filepath.IsAbs(e.RemoteCacheDir) || strings.HasPrefix(e.RemoteCacheDir, "~") {
|
|
remoteCacheDir, err := execext.ExpandLiteral(e.RemoteCacheDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.TempDir.Remote = remoteCacheDir
|
|
} else {
|
|
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, e.RemoteCacheDir)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Executor) setupStdFiles() {
|
|
if e.Stdin == nil {
|
|
e.Stdin = os.Stdin
|
|
}
|
|
if e.Stdout == nil {
|
|
e.Stdout = os.Stdout
|
|
}
|
|
if e.Stderr == nil {
|
|
e.Stderr = os.Stderr
|
|
}
|
|
}
|
|
|
|
func (e *Executor) setupLogger() {
|
|
e.Logger = &logger.Logger{
|
|
Stdin: e.Stdin,
|
|
Stdout: e.Stdout,
|
|
Stderr: e.Stderr,
|
|
Verbose: e.Verbose,
|
|
Color: e.Color,
|
|
AssumeYes: e.AssumeYes,
|
|
AssumeTerm: e.AssumeTerm,
|
|
}
|
|
}
|
|
|
|
func (e *Executor) setupOutput() error {
|
|
if !e.OutputStyle.IsSet() {
|
|
e.OutputStyle = e.Taskfile.Output
|
|
}
|
|
if !e.OutputStyle.IsSet() && e.OutputCIAuto {
|
|
if name := detectCIOutput(); name != "" {
|
|
e.OutputStyle.Name = name
|
|
}
|
|
}
|
|
|
|
var err error
|
|
e.Output, err = output.BuildFor(&e.OutputStyle, e.Logger)
|
|
return err
|
|
}
|
|
|
|
// detectCIOutput returns the name of a CI-aware output style to use based
|
|
// on environment variables set by common CI runners. Returns an empty string
|
|
// when no supported CI environment is detected.
|
|
func detectCIOutput() string {
|
|
if isGitLab, _ := strconv.ParseBool(os.Getenv("GITLAB_CI")); isGitLab {
|
|
return "gitlab"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (e *Executor) setupCompiler() error {
|
|
if e.UserWorkingDir == "" {
|
|
var err error
|
|
e.UserWorkingDir, err = os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
e.Compiler = &Compiler{
|
|
Dir: e.Dir,
|
|
Entrypoint: e.Entrypoint,
|
|
UserWorkingDir: e.UserWorkingDir,
|
|
TaskfileEnv: e.Taskfile.Env,
|
|
TaskfileVars: e.Taskfile.Vars,
|
|
Logger: e.Logger,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *Executor) readDotEnvFiles() error {
|
|
if e.Taskfile == nil || len(e.Taskfile.Dotenv) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if e.Taskfile.Version.LessThan(ast.V3) {
|
|
return nil
|
|
}
|
|
|
|
vars, err := e.Compiler.GetTaskfileVariables()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
env, err := taskfile.Dotenv(vars, e.Taskfile, e.Dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for k, v := range env.All() {
|
|
if _, ok := e.Taskfile.Env.Get(k); !ok {
|
|
e.Taskfile.Env.Set(k, v)
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (e *Executor) setupDefaults() {
|
|
if e.Taskfile.Method == "" {
|
|
e.Taskfile.Method = "checksum"
|
|
}
|
|
if e.Taskfile.Run == "" {
|
|
e.Taskfile.Run = "always"
|
|
}
|
|
}
|
|
|
|
func (e *Executor) setupConcurrencyState() {
|
|
e.executionHashes = make(map[string]context.Context)
|
|
|
|
e.taskCallCount = make(map[string]*int32, e.Taskfile.Tasks.Len())
|
|
e.mkdirMutexMap = make(map[string]*sync.Mutex, e.Taskfile.Tasks.Len())
|
|
for k := range e.Taskfile.Tasks.Keys(nil) {
|
|
e.taskCallCount[k] = new(int32)
|
|
e.mkdirMutexMap[k] = &sync.Mutex{}
|
|
}
|
|
|
|
if e.Concurrency > 0 {
|
|
e.concurrencySemaphore = make(chan struct{}, e.Concurrency)
|
|
}
|
|
}
|
|
|
|
func (e *Executor) doVersionChecks() error {
|
|
if !e.EnableVersionCheck {
|
|
return nil
|
|
}
|
|
// Copy the version to avoid modifying the original
|
|
schemaVersion := &semver.Version{}
|
|
*schemaVersion = *e.Taskfile.Version
|
|
|
|
// Error if the Taskfile uses a schema version below v3
|
|
if schemaVersion.LessThan(ast.V3) {
|
|
return &errors.TaskfileVersionCheckError{
|
|
URI: e.Taskfile.Location,
|
|
SchemaVersion: schemaVersion,
|
|
Message: `no longer supported. Please use v3 or above`,
|
|
}
|
|
}
|
|
|
|
// Get the current version of Task
|
|
// If we can't parse the version (e.g. when its "devel"), then ignore the current version checks
|
|
currentVersion, err := semver.NewVersion(version.GetVersion())
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
// Error if the Taskfile uses a schema version above the current version of Task
|
|
if schemaVersion.GreaterThan(currentVersion) {
|
|
return &errors.TaskfileVersionCheckError{
|
|
URI: e.Taskfile.Location,
|
|
SchemaVersion: schemaVersion,
|
|
Message: fmt.Sprintf(`is greater than the current version of Task (%s)`, currentVersion.String()),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|