mirror of
https://github.com/go-task/task.git
synced 2026-04-24 19:54:16 +02:00
b8abadb4f0
Previously the gitlab output wrapped each command individually, causing two visible bugs in real GitLab pipelines: - every section displayed a duration of 00:00, because start and end markers were emitted microseconds apart for instant commands - the `task: [NAME] CMD` announcement lines were rendered outside the sections, because Logger.Errf bypassed the cmd-level wrapper Fix by wrapping output at the task level via a new optional [output.TaskWrapper] interface that GitLab implements. Task-scoped writers are threaded via ctx so nested `task:` invocations produce properly nested sections (GitLab supports this natively), and deps running in parallel each get their own buffer with mutex-protected flushes into the parent's buffer. - `internal/output/output.go`: add TaskWrapper interface - `internal/output/gitlab.go`: logic moved from WrapWriter to WrapTask; WrapWriter becomes passthrough; sync.Mutex around the buffer for concurrent flushes from parallel sub-task sections - `task_output.go` (new): ctx plumbing + helpers kept out of task.go - `task.go`: 7 lines of surgical edits — name the lambda's error return, wrap before the cmd loop, defer the closer with the final error, and swap the cmd announcement to `printCmdAnnouncement` which writes into the task-scoped stderr
71 lines
2.3 KiB
Go
71 lines
2.3 KiB
Go
package task
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
|
|
"github.com/fatih/color"
|
|
|
|
"github.com/go-task/task/v3/internal/logger"
|
|
"github.com/go-task/task/v3/internal/output"
|
|
"github.com/go-task/task/v3/internal/templater"
|
|
"github.com/go-task/task/v3/taskfile/ast"
|
|
)
|
|
|
|
type taskWritersKey struct{}
|
|
|
|
type taskWriters struct {
|
|
stdout, stderr io.Writer
|
|
}
|
|
|
|
// writersFromCtx returns the task-scoped writers if set, otherwise the
|
|
// Executor's own stdout/stderr.
|
|
func (e *Executor) writersFromCtx(ctx context.Context) (io.Writer, io.Writer) {
|
|
if tw, ok := ctx.Value(taskWritersKey{}).(*taskWriters); ok && tw != nil {
|
|
return tw.stdout, tw.stderr
|
|
}
|
|
return e.Stdout, e.Stderr
|
|
}
|
|
|
|
// wrapTaskOutput wraps a task's output in a task-scoped block if e.Output
|
|
// implements [output.TaskWrapper] and the task is not interactive. Returns
|
|
// the (possibly updated) ctx and a closer that flushes the block. The closer
|
|
// is always safe to call — it is a no-op when no wrapping took place.
|
|
func (e *Executor) wrapTaskOutput(ctx context.Context, t *ast.Task, call *Call) (context.Context, func(error)) {
|
|
noop := func(error) {}
|
|
if t.Interactive {
|
|
return ctx, noop
|
|
}
|
|
tw, ok := e.Output.(output.TaskWrapper)
|
|
if !ok {
|
|
return ctx, noop
|
|
}
|
|
stdOut, stdErr := e.writersFromCtx(ctx)
|
|
vars, err := e.Compiler.FastGetVariables(t, call)
|
|
if err != nil {
|
|
e.Logger.VerboseErrf(logger.Yellow, "task: output setup: %v\n", err)
|
|
return ctx, noop
|
|
}
|
|
wOut, wErr, closer := tw.WrapTask(stdOut, stdErr, &templater.Cache{Vars: vars})
|
|
ctx = context.WithValue(ctx, taskWritersKey{}, &taskWriters{stdout: wOut, stderr: wErr})
|
|
return ctx, func(loopErr error) {
|
|
if err := closer(loopErr); err != nil {
|
|
e.Logger.Errf(logger.Red, "task: output close: %v\n", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// printCmdAnnouncement prints the "task: [NAME] CMD" line using the
|
|
// task-scoped stderr if available, so the announcement ends up inside the
|
|
// task's output block.
|
|
func (e *Executor) printCmdAnnouncement(ctx context.Context, t *ast.Task, cmdStr string) {
|
|
_, stdErr := e.writersFromCtx(ctx)
|
|
if stdErr == e.Stderr {
|
|
// No task-scoped writer — fall back to the Logger to preserve existing
|
|
// behavior (respects Logger's color config, etc.).
|
|
e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmdStr)
|
|
return
|
|
}
|
|
_, _ = color.New(color.FgGreen).Fprintf(stdErr, "task: [%s] %s\n", t.Name(), cmdStr)
|
|
}
|