Files
go-task/internal/output/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

64 lines
1.7 KiB
Go

package output
import (
"fmt"
"io"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast"
)
type Output interface {
WrapWriter(stdOut, stdErr io.Writer, prefix string, cache *templater.Cache) (io.Writer, io.Writer, CloseFunc)
}
// TaskWrapper is an optional interface that Output implementations can satisfy
// to wrap an entire task's execution in a single enclosing block — including
// the task's command announcements and all its commands' output — instead of
// wrapping each command individually via WrapWriter.
type TaskWrapper interface {
WrapTask(stdOut, stdErr io.Writer, cache *templater.Cache) (io.Writer, io.Writer, CloseFunc)
}
type CloseFunc func(err error) error
// Build the Output for the requested ast.Output.
func BuildFor(o *ast.Output, logger *logger.Logger) (Output, error) {
switch o.Name {
case "interleaved", "":
if err := checkOutputGroupUnset(o); err != nil {
return nil, err
}
return Interleaved{}, nil
case "group":
return Group{
Begin: o.Group.Begin,
End: o.Group.End,
ErrorOnly: o.Group.ErrorOnly,
}, nil
case "prefixed":
if err := checkOutputGroupUnset(o); err != nil {
return nil, err
}
return NewPrefixed(logger), nil
case "gitlab":
if err := checkOutputGroupUnset(o); err != nil {
return nil, err
}
return GitLab{
Collapsed: o.GitLab.Collapsed,
ErrorOnly: o.GitLab.ErrorOnly,
}, nil
default:
return nil, fmt.Errorf(`task: output style %q not recognized`, o.Name)
}
}
func checkOutputGroupUnset(o *ast.Output) error {
if o.Group.IsSet() {
return fmt.Errorf("task: output style %q does not support the group begin/end parameter", o.Name)
}
return nil
}