1
0
mirror of https://github.com/go-task/task.git synced 2026-04-24 19:54:16 +02:00
Files
task/task_output.go
Valentin Maerten b8abadb4f0 🐛 fix(output): wrap gitlab sections at task level (#2806)
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
2026-04-22 17:22:54 +02:00

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