Skip to content

Commit cfb6653

Browse files
committed
Merge branch 'group-begin-message' of https://github.com/janslow/task into janslow-group-begin-message
2 parents 51c6ebc + 74f5cf8 commit cfb6653

14 files changed

Lines changed: 268 additions & 34 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ jobs:
2727
run: go build -o ./bin/task -v ./cmd/task
2828

2929
- name: Test
30-
run: ./bin/task test
30+
run: ./bin/task test --output=group --output-group-begin='::group::{{.TASK}}' --output-group-end='::endgroup::'

cmd/task/task.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212
"syscall"
1313

14+
outputpkg "github.com/go-task/task/v3/internal/output"
1415
"github.com/spf13/pflag"
1516
"mvdan.cc/sh/v3/syntax"
1617

@@ -72,7 +73,7 @@ func main() {
7273
concurrency int
7374
dir string
7475
entrypoint string
75-
output string
76+
output outputpkg.Style
7677
color bool
7778
)
7879

@@ -91,7 +92,9 @@ func main() {
9192
pflag.BoolVar(&summary, "summary", false, "show summary about a task")
9293
pflag.StringVarP(&dir, "dir", "d", "", "sets directory of execution")
9394
pflag.StringVarP(&entrypoint, "taskfile", "t", "", `choose which Taskfile to run. Defaults to "Taskfile.yml"`)
94-
pflag.StringVarP(&output, "output", "o", "", "sets output style: [interleaved|group|prefixed]")
95+
pflag.StringVarP(&output.Name, "output", "o", "", "sets output style: [interleaved|group|prefixed]")
96+
pflag.StringVar(&output.Group.Begin, "output-group-begin", "", "message template to print before a task's grouped output")
97+
pflag.StringVar(&output.Group.End, "output-group-end", "", "message template to print after a task's grouped output")
9598
pflag.BoolVarP(&color, "color", "c", true, "colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable")
9699
pflag.IntVarP(&concurrency, "concurrency", "C", 0, "limit number tasks to run concurrently")
97100
pflag.Parse()
@@ -126,6 +129,17 @@ func main() {
126129
entrypoint = filepath.Base(entrypoint)
127130
}
128131

132+
if output.Name != "group" {
133+
if output.Group.Begin != "" {
134+
log.Fatal("task: You can't set --output-group-begin without --output=group")
135+
return
136+
}
137+
if output.Group.End != "" {
138+
log.Fatal("task: You can't set --output-group-end without --output=group")
139+
return
140+
}
141+
}
142+
129143
e := task.Executor{
130144
Force: force,
131145
Watch: watch,

docs/usage.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,33 @@ tasks:
973973
finishes, so you won't have live feedback for commands that take a long time
974974
to run.
975975

976+
When using the `group` output, you can optionally provide a templated message
977+
to print at the start of the group. This can be useful for instructing CI
978+
systems to group all of the output for a given task, such as with [GitHub
979+
Actions' `::group::` command](https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#grouping-log-lines).
980+
981+
```yaml
982+
version: '3'
983+
984+
output:
985+
group:
986+
begin: '::begin::{{.TASK}}'
987+
end: '::endgroup::'
988+
989+
tasks:
990+
default:
991+
cmds:
992+
- echo 'Hello, World!'
993+
silent: true
994+
```
995+
996+
```bash
997+
$ task default
998+
::begin::default
999+
Hello, World!
1000+
::endgroup::
1001+
```
1002+
9761003
The `prefix` output will prefix every line printed by a command with
9771004
`[task-name] ` as the prefix, but you can customize the prefix for a command
9781005
with the `prefix:` attribute:

internal/output/group.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,40 @@ import (
55
"io"
66
)
77

8-
type Group struct{}
8+
type Group struct{
9+
Begin, End string
10+
}
911

10-
func (Group) WrapWriter(w io.Writer, _ string) io.Writer {
11-
return &groupWriter{writer: w}
12+
func (g Group) WrapWriter(w io.Writer, _ string, tmpl Templater) io.Writer {
13+
gw := &groupWriter{writer: w}
14+
if g.Begin != "" {
15+
gw.begin = tmpl.Replace(g.Begin) + "\n"
16+
}
17+
if g.End != "" {
18+
gw.end = tmpl.Replace(g.End) + "\n"
19+
}
20+
return gw
1221
}
1322

1423
type groupWriter struct {
1524
writer io.Writer
1625
buff bytes.Buffer
26+
begin, end string
1727
}
1828

1929
func (gw *groupWriter) Write(p []byte) (int, error) {
2030
return gw.buff.Write(p)
2131
}
2232

2333
func (gw *groupWriter) Close() error {
34+
if gw.buff.Len() == 0 {
35+
// don't print begin/end messages if there's no buffered entries
36+
return nil
37+
}
38+
if _, err := io.WriteString(gw.writer, gw.begin); err != nil {
39+
return err
40+
}
41+
gw.buff.WriteString(gw.end)
2442
_, err := io.Copy(gw.writer, &gw.buff)
2543
return err
2644
}

internal/output/interleaved.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ import (
66

77
type Interleaved struct{}
88

9-
func (Interleaved) WrapWriter(w io.Writer, _ string) io.Writer {
9+
func (Interleaved) WrapWriter(w io.Writer, _ string, _ Templater) io.Writer {
1010
return w
1111
}

internal/output/output.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import (
44
"io"
55
)
66

7+
8+
// Templater executes a template engine.
9+
// It is provided by the templater.Templater package.
10+
type Templater interface {
11+
// Replace replaces the provided template string with a rendered string.
12+
Replace(tmpl string) string
13+
}
14+
715
type Output interface {
8-
WrapWriter(w io.Writer, prefix string) io.Writer
16+
WrapWriter(w io.Writer, prefix string, tmpl Templater) io.Writer
917
}

internal/output/output_test.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"io"
77
"testing"
88

9+
"github.com/go-task/task/v3/internal/templater"
10+
"github.com/go-task/task/v3/taskfile"
911
"github.com/stretchr/testify/assert"
1012

1113
"github.com/go-task/task/v3/internal/output"
@@ -14,7 +16,7 @@ import (
1416
func TestInterleaved(t *testing.T) {
1517
var b bytes.Buffer
1618
var o output.Output = output.Interleaved{}
17-
var w = o.WrapWriter(&b, "")
19+
var w = o.WrapWriter(&b, "", nil)
1820

1921
fmt.Fprintln(w, "foo\nbar")
2022
assert.Equal(t, "foo\nbar\n", b.String())
@@ -25,7 +27,7 @@ func TestInterleaved(t *testing.T) {
2527
func TestGroup(t *testing.T) {
2628
var b bytes.Buffer
2729
var o output.Output = output.Group{}
28-
var w = o.WrapWriter(&b, "").(io.WriteCloser)
30+
var w = o.WrapWriter(&b, "", nil).(io.WriteCloser)
2931

3032
fmt.Fprintln(w, "foo\nbar")
3133
assert.Equal(t, "", b.String())
@@ -35,10 +37,43 @@ func TestGroup(t *testing.T) {
3537
assert.Equal(t, "foo\nbar\nbaz\n", b.String())
3638
}
3739

40+
func TestGroupWithBeginEnd(t *testing.T) {
41+
tmpl := templater.Templater{
42+
Vars: &taskfile.Vars{
43+
Keys: []string{"VAR1"},
44+
Mapping: map[string]taskfile.Var{
45+
"VAR1": {Static: "example-value"},
46+
},
47+
},
48+
}
49+
50+
var o output.Output = output.Group{
51+
Begin: "::group::{{ .VAR1 }}",
52+
End: "::endgroup::",
53+
}
54+
t.Run("simple", func(t *testing.T) {
55+
var b bytes.Buffer
56+
var w = o.WrapWriter(&b, "", &tmpl).(io.WriteCloser)
57+
58+
fmt.Fprintln(w, "foo\nbar")
59+
assert.Equal(t, "", b.String())
60+
fmt.Fprintln(w, "baz")
61+
assert.Equal(t, "", b.String())
62+
assert.NoError(t, w.Close())
63+
assert.Equal(t, "::group::example-value\nfoo\nbar\nbaz\n::endgroup::\n", b.String())
64+
})
65+
t.Run("no output", func(t *testing.T) {
66+
var b bytes.Buffer
67+
var w = o.WrapWriter(&b, "", &tmpl).(io.WriteCloser)
68+
assert.NoError(t, w.Close())
69+
assert.Equal(t, "", b.String())
70+
})
71+
}
72+
3873
func TestPrefixed(t *testing.T) {
3974
var b bytes.Buffer
4075
var o output.Output = output.Prefixed{}
41-
var w = o.WrapWriter(&b, "prefix").(io.WriteCloser)
76+
var w = o.WrapWriter(&b, "prefix", nil).(io.WriteCloser)
4277

4378
t.Run("simple use cases", func(t *testing.T) {
4479
b.Reset()

internal/output/prefixed.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99

1010
type Prefixed struct{}
1111

12-
func (Prefixed) WrapWriter(w io.Writer, prefix string) io.Writer {
12+
func (Prefixed) WrapWriter(w io.Writer, prefix string, _ Templater) io.Writer {
1313
return &prefixWriter{writer: w, prefix: prefix}
1414
}
1515

internal/output/style.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package output
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
// Style of the Task output
8+
type Style struct {
9+
// Name of the Style.
10+
Name string `yaml:"-"`
11+
// Group specific style
12+
Group GroupStyle
13+
}
14+
15+
// Build the Output for the requested Style.
16+
func (s *Style) Build() (Output, error) {
17+
switch s.Name {
18+
case "interleaved", "":
19+
return Interleaved{}, s.ensureGroupStyleUnset()
20+
case "group":
21+
return Group{
22+
Begin: s.Group.Begin,
23+
End: s.Group.End,
24+
}, nil
25+
case "prefixed":
26+
return Prefixed{}, s.ensureGroupStyleUnset()
27+
default:
28+
return nil, fmt.Errorf(`task: output style %q not recognized`, s.Name)
29+
}
30+
}
31+
32+
func (s *Style) ensureGroupStyleUnset() error {
33+
if s.Group.IsSet() {
34+
return fmt.Errorf("task: output style %q does not support the group begin/end parameter", s.Name)
35+
}
36+
return nil
37+
}
38+
39+
// IsSet returns true if and only if a custom output style is set.
40+
func (s *Style) IsSet() bool {
41+
return s.Name != ""
42+
}
43+
44+
// UnmarshalYAML implements yaml.Unmarshaler
45+
// It accepts a scalar node representing the Style.Name or a mapping node representing the GroupStyle.
46+
func (s *Style) UnmarshalYAML(unmarshal func(interface{}) error) error {
47+
var name string
48+
if err := unmarshal(&name); err == nil {
49+
return s.UnmarshalText([]byte(name))
50+
}
51+
var tmp struct {
52+
Group *GroupStyle
53+
}
54+
if err := unmarshal(&tmp); err != nil {
55+
return fmt.Errorf("task: output style must be a string or mapping with a \"group\" key: %w", err)
56+
}
57+
if tmp.Group == nil {
58+
return fmt.Errorf("task: output style must have the \"group\" key when in mapping form")
59+
}
60+
*s = Style{
61+
Name: "group",
62+
Group: *tmp.Group,
63+
}
64+
return nil
65+
}
66+
67+
// UnmarshalText implements encoding.TextUnmarshaler
68+
// It accepts the Style.Node
69+
func (s *Style) UnmarshalText(text []byte) error {
70+
tmp := Style{Name: string(text)}
71+
if _, err := tmp.Build(); err != nil {
72+
return err
73+
}
74+
return nil
75+
}
76+
77+
// GroupStyle is the style options specific to the Group style.
78+
type GroupStyle struct{
79+
Begin, End string
80+
}
81+
82+
// IsSet returns true if and only if a custom output style is set.
83+
func (g *GroupStyle) IsSet() bool {
84+
return g != nil && *g != GroupStyle{}
85+
}

0 commit comments

Comments
 (0)