mirror of
https://github.com/go-task/task.git
synced 2026-06-11 09:51:50 +00:00
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
117 lines
2.9 KiB
Go
117 lines
2.9 KiB
Go
package output
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/go-task/task/v3/internal/templater"
|
|
)
|
|
|
|
// GitLab renders a task's output wrapped in [GitLab CI collapsible
|
|
// section markers]. Section IDs are generated automatically so that
|
|
// start and end markers always match and stay unique per invocation.
|
|
//
|
|
// GitLab wraps output at the task level via the [TaskWrapper] interface,
|
|
// so each task (including its command announcements and all its cmds)
|
|
// appears inside a single collapsible section. Nested task invocations
|
|
// produce nested sections.
|
|
//
|
|
// [GitLab CI collapsible section markers]: https://docs.gitlab.com/ci/jobs/job_logs/#create-custom-collapsible-sections
|
|
type GitLab struct {
|
|
Collapsed bool
|
|
ErrorOnly bool
|
|
}
|
|
|
|
// WrapWriter is a passthrough for GitLab: wrapping happens at the task
|
|
// level via WrapTask, not per command.
|
|
func (g GitLab) WrapWriter(stdOut, stdErr io.Writer, _ string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) {
|
|
return stdOut, stdErr, func(error) error { return nil }
|
|
}
|
|
|
|
// WrapTask wraps an entire task's output in a single collapsible section.
|
|
func (g GitLab) WrapTask(stdOut, _ io.Writer, cache *templater.Cache) (io.Writer, io.Writer, CloseFunc) {
|
|
header := ""
|
|
if cache != nil {
|
|
header = templater.Replace("{{.TASK}}", cache)
|
|
}
|
|
if header == "" {
|
|
header = "task"
|
|
}
|
|
|
|
id := fmt.Sprintf("%s_%s", gitlabSectionSlug(header), uuid.New().String()[:8])
|
|
|
|
gw := &gitlabWriter{
|
|
writer: stdOut,
|
|
id: id,
|
|
header: header,
|
|
collapsed: g.Collapsed,
|
|
startTS: time.Now().Unix(),
|
|
}
|
|
|
|
return gw, gw, func(err error) error {
|
|
if g.ErrorOnly && err == nil {
|
|
return nil
|
|
}
|
|
return gw.close()
|
|
}
|
|
}
|
|
|
|
type gitlabWriter struct {
|
|
mu sync.Mutex
|
|
writer io.Writer
|
|
buff bytes.Buffer
|
|
id string
|
|
header string
|
|
collapsed bool
|
|
startTS int64
|
|
}
|
|
|
|
func (gw *gitlabWriter) Write(p []byte) (int, error) {
|
|
gw.mu.Lock()
|
|
defer gw.mu.Unlock()
|
|
return gw.buff.Write(p)
|
|
}
|
|
|
|
func (gw *gitlabWriter) close() error {
|
|
gw.mu.Lock()
|
|
defer gw.mu.Unlock()
|
|
|
|
if gw.buff.Len() == 0 {
|
|
return nil
|
|
}
|
|
|
|
var b bytes.Buffer
|
|
b.WriteString(gitlabSectionStart(gw.startTS, gw.id, gw.header, gw.collapsed))
|
|
if _, err := io.Copy(&b, &gw.buff); err != nil {
|
|
return err
|
|
}
|
|
b.WriteString(gitlabSectionEnd(time.Now().Unix(), gw.id))
|
|
|
|
_, err := io.Copy(gw.writer, &b)
|
|
return err
|
|
}
|
|
|
|
func gitlabSectionStart(ts int64, id, header string, collapsed bool) string {
|
|
options := ""
|
|
if collapsed {
|
|
options = "[collapsed=true]"
|
|
}
|
|
return fmt.Sprintf("\x1b[0Ksection_start:%d:%s%s\r\x1b[0K%s\n", ts, id, options, header)
|
|
}
|
|
|
|
func gitlabSectionEnd(ts int64, id string) string {
|
|
return fmt.Sprintf("\x1b[0Ksection_end:%d:%s\r\x1b[0K\n", ts, id)
|
|
}
|
|
|
|
var gitlabSlugDisallowed = regexp.MustCompile(`[^a-zA-Z0-9_.-]`)
|
|
|
|
func gitlabSectionSlug(s string) string {
|
|
return gitlabSlugDisallowed.ReplaceAllString(s, "_")
|
|
}
|