mirror of
https://github.com/go-task/task.git
synced 2025-08-10 22:42:19 +02:00
Add support for begin/end messages with grouped output
Fixes #647 This allows CI systems that support grouping (such as with [GitHub Actions's `::group::` command](https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#grouping-log-lines) and [Azure Devops](https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#formatting-commands)) to collapse all of the logs for a single task, to improve readability of logs ## Example The following Taskfile ``` # Taskfile.yml version: 3 output: group: begin: "::group::{{ .TASK }}" end: "::endgroup::" tasks: default: cmds: - "echo 'Hello, World!'" ``` Results in the following output ```bash $ task task: [default] echo 'Hello, World!' ::group::default Hello, World! ::endgroup:: ``` See [this GitHub Actions job](https://github.com/janslow/task/runs/4811059609?check_suite_focus=true) for a full example <img width="771" alt="image" src="https://user-images.githubusercontent.com/1253367/149429832-6cb0c1b5-0758-442e-9375-c4daa65771bc.png"> <img width="394" alt="image" src="https://user-images.githubusercontent.com/1253367/149429851-1d5d2ab5-9095-4795-9b57-f91750720d40.png">
This commit is contained in:
@@ -5,15 +5,25 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type Group struct{}
|
||||
type Group struct{
|
||||
Begin, End string
|
||||
}
|
||||
|
||||
func (Group) WrapWriter(w io.Writer, _ string) io.Writer {
|
||||
return &groupWriter{writer: w}
|
||||
func (g Group) WrapWriter(w io.Writer, _ string, tmpl Templater) io.Writer {
|
||||
gw := &groupWriter{writer: w}
|
||||
if g.Begin != "" {
|
||||
gw.begin = tmpl.Replace(g.Begin) + "\n"
|
||||
}
|
||||
if g.End != "" {
|
||||
gw.end = tmpl.Replace(g.End) + "\n"
|
||||
}
|
||||
return gw
|
||||
}
|
||||
|
||||
type groupWriter struct {
|
||||
writer io.Writer
|
||||
buff bytes.Buffer
|
||||
begin, end string
|
||||
}
|
||||
|
||||
func (gw *groupWriter) Write(p []byte) (int, error) {
|
||||
@@ -21,6 +31,14 @@ func (gw *groupWriter) Write(p []byte) (int, error) {
|
||||
}
|
||||
|
||||
func (gw *groupWriter) Close() error {
|
||||
if gw.buff.Len() == 0 {
|
||||
// don't print begin/end messages if there's no buffered entries
|
||||
return nil
|
||||
}
|
||||
if _, err := io.WriteString(gw.writer, gw.begin); err != nil {
|
||||
return err
|
||||
}
|
||||
gw.buff.WriteString(gw.end)
|
||||
_, err := io.Copy(gw.writer, &gw.buff)
|
||||
return err
|
||||
}
|
||||
|
@@ -6,6 +6,6 @@ import (
|
||||
|
||||
type Interleaved struct{}
|
||||
|
||||
func (Interleaved) WrapWriter(w io.Writer, _ string) io.Writer {
|
||||
func (Interleaved) WrapWriter(w io.Writer, _ string, _ Templater) io.Writer {
|
||||
return w
|
||||
}
|
||||
|
@@ -4,6 +4,14 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type Output interface {
|
||||
WrapWriter(w io.Writer, prefix string) io.Writer
|
||||
|
||||
// Templater executes a template engine.
|
||||
// It is provided by the templater.Templater package.
|
||||
type Templater interface {
|
||||
// Replace replaces the provided template string with a rendered string.
|
||||
Replace(tmpl string) string
|
||||
}
|
||||
|
||||
type Output interface {
|
||||
WrapWriter(w io.Writer, prefix string, tmpl Templater) io.Writer
|
||||
}
|
||||
|
@@ -6,6 +6,8 @@ import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/go-task/task/v3/internal/output"
|
||||
@@ -14,7 +16,7 @@ import (
|
||||
func TestInterleaved(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
var o output.Output = output.Interleaved{}
|
||||
var w = o.WrapWriter(&b, "")
|
||||
var w = o.WrapWriter(&b, "", nil)
|
||||
|
||||
fmt.Fprintln(w, "foo\nbar")
|
||||
assert.Equal(t, "foo\nbar\n", b.String())
|
||||
@@ -25,7 +27,7 @@ func TestInterleaved(t *testing.T) {
|
||||
func TestGroup(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
var o output.Output = output.Group{}
|
||||
var w = o.WrapWriter(&b, "").(io.WriteCloser)
|
||||
var w = o.WrapWriter(&b, "", nil).(io.WriteCloser)
|
||||
|
||||
fmt.Fprintln(w, "foo\nbar")
|
||||
assert.Equal(t, "", b.String())
|
||||
@@ -35,10 +37,43 @@ func TestGroup(t *testing.T) {
|
||||
assert.Equal(t, "foo\nbar\nbaz\n", b.String())
|
||||
}
|
||||
|
||||
func TestGroupWithBeginEnd(t *testing.T) {
|
||||
tmpl := templater.Templater{
|
||||
Vars: &taskfile.Vars{
|
||||
Keys: []string{"VAR1"},
|
||||
Mapping: map[string]taskfile.Var{
|
||||
"VAR1": {Static: "example-value"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var o output.Output = output.Group{
|
||||
Begin: "::group::{{ .VAR1 }}",
|
||||
End: "::endgroup::",
|
||||
}
|
||||
t.Run("simple", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
var w = o.WrapWriter(&b, "", &tmpl).(io.WriteCloser)
|
||||
|
||||
fmt.Fprintln(w, "foo\nbar")
|
||||
assert.Equal(t, "", b.String())
|
||||
fmt.Fprintln(w, "baz")
|
||||
assert.Equal(t, "", b.String())
|
||||
assert.NoError(t, w.Close())
|
||||
assert.Equal(t, "::group::example-value\nfoo\nbar\nbaz\n::endgroup::\n", b.String())
|
||||
})
|
||||
t.Run("no output", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
var w = o.WrapWriter(&b, "", &tmpl).(io.WriteCloser)
|
||||
assert.NoError(t, w.Close())
|
||||
assert.Equal(t, "", b.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrefixed(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
var o output.Output = output.Prefixed{}
|
||||
var w = o.WrapWriter(&b, "prefix").(io.WriteCloser)
|
||||
var w = o.WrapWriter(&b, "prefix", nil).(io.WriteCloser)
|
||||
|
||||
t.Run("simple use cases", func(t *testing.T) {
|
||||
b.Reset()
|
||||
|
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
type Prefixed struct{}
|
||||
|
||||
func (Prefixed) WrapWriter(w io.Writer, prefix string) io.Writer {
|
||||
func (Prefixed) WrapWriter(w io.Writer, prefix string, _ Templater) io.Writer {
|
||||
return &prefixWriter{writer: w, prefix: prefix}
|
||||
}
|
||||
|
||||
|
85
internal/output/style.go
Normal file
85
internal/output/style.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Style of the Task output
|
||||
type Style struct {
|
||||
// Name of the Style.
|
||||
Name string `yaml:"-"`
|
||||
// Group specific style
|
||||
Group GroupStyle
|
||||
}
|
||||
|
||||
// Build the Output for the requested Style.
|
||||
func (s *Style) Build() (Output, error) {
|
||||
switch s.Name {
|
||||
case "interleaved", "":
|
||||
return Interleaved{}, s.ensureGroupStyleUnset()
|
||||
case "group":
|
||||
return Group{
|
||||
Begin: s.Group.Begin,
|
||||
End: s.Group.End,
|
||||
}, nil
|
||||
case "prefixed":
|
||||
return Prefixed{}, s.ensureGroupStyleUnset()
|
||||
default:
|
||||
return nil, fmt.Errorf(`task: output style %q not recognized`, s.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Style) ensureGroupStyleUnset() error {
|
||||
if s.Group.IsSet() {
|
||||
return fmt.Errorf("task: output style %q does not support the group begin/end parameter", s.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsSet returns true if and only if a custom output style is set.
|
||||
func (s *Style) IsSet() bool {
|
||||
return s.Name != ""
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler
|
||||
// It accepts a scalar node representing the Style.Name or a mapping node representing the GroupStyle.
|
||||
func (s *Style) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var name string
|
||||
if err := unmarshal(&name); err == nil {
|
||||
return s.UnmarshalText([]byte(name))
|
||||
}
|
||||
var tmp struct {
|
||||
Group *GroupStyle
|
||||
}
|
||||
if err := unmarshal(&tmp); err != nil {
|
||||
return fmt.Errorf("task: output style must be a string or mapping with a \"group\" key: %w", err)
|
||||
}
|
||||
if tmp.Group == nil {
|
||||
return fmt.Errorf("task: output style must have the \"group\" key when in mapping form")
|
||||
}
|
||||
*s = Style{
|
||||
Name: "group",
|
||||
Group: *tmp.Group,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler
|
||||
// It accepts the Style.Node
|
||||
func (s *Style) UnmarshalText(text []byte) error {
|
||||
tmp := Style{Name: string(text)}
|
||||
if _, err := tmp.Build(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GroupStyle is the style options specific to the Group style.
|
||||
type GroupStyle struct{
|
||||
Begin, End string
|
||||
}
|
||||
|
||||
// IsSet returns true if and only if a custom output style is set.
|
||||
func (g *GroupStyle) IsSet() bool {
|
||||
return g != nil && *g != GroupStyle{}
|
||||
}
|
Reference in New Issue
Block a user