From 542fe465e9ec20f8638db74fe9d7e733e965d2e7 Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Wed, 22 Apr 2026 14:10:55 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(output):=20add=20gitlab=20outp?= =?UTF-8?q?ut=20mode=20(#2806)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new `gitlab` output style that wraps each task's output in GitLab CI collapsible section markers. Section IDs are generated automatically so that start and end markers always match and stay unique per invocation — even when the same task runs multiple times in one job. Options: `collapsed` (maps to GitLab's native `[collapsed=true]`) and `error_only` (Task-level behavior, identical to `group.error_only`). Also introduces `output-ci-auto` (taskrc + TASK_OUTPUT_CI_AUTO env var) that auto-selects a CI-aware output style when a supported CI runner is detected (currently `GITLAB_CI=true` → gitlab) and no output style is explicitly configured. Keeps the Taskfile neutral so local devs are not forced into CI-shaped output. Refs #2806. --- executor.go | 16 ++++ internal/flags/flags.go | 5 +- internal/output/gitlab.go | 97 +++++++++++++++++++ internal/output/output.go | 8 ++ internal/output/output_test.go | 112 ++++++++++++++++++++++ setup.go | 16 ++++ setup_test.go | 97 +++++++++++++++++++ taskfile/ast/output.go | 35 +++++-- taskrc/ast/taskrc.go | 2 + taskrc/taskrc_test.go | 23 +++++ website/src/docs/guide.md | 63 +++++++++++- website/src/docs/reference/cli.md | 3 +- website/src/docs/reference/config.md | 15 +++ website/src/docs/reference/environment.md | 10 ++ website/src/docs/reference/schema.md | 2 +- website/src/public/schema.json | 18 +++- 16 files changed, 509 insertions(+), 13 deletions(-) create mode 100644 internal/output/gitlab.go create mode 100644 setup_test.go diff --git a/executor.go b/executor.go index 43b8bacf..2f68bd73 100644 --- a/executor.go +++ b/executor.go @@ -67,6 +67,7 @@ type ( Compiler *Compiler Output output.Output OutputStyle ast.Output + OutputCIAuto bool TaskSorter sort.Sorter UserWorkingDir string EnableVersionCheck bool @@ -522,6 +523,21 @@ func (o *outputStyleOption) ApplyToExecutor(e *Executor) { e.OutputStyle = o.outputStyle } +// WithOutputCIAuto enables automatic selection of a CI-aware output style +// (e.g. "gitlab") when a supported CI environment is detected and no explicit +// output style is configured in the Taskfile or via CLI. +func WithOutputCIAuto(enabled bool) ExecutorOption { + return &outputCIAutoOption{enabled} +} + +type outputCIAutoOption struct { + enabled bool +} + +func (o *outputCIAutoOption) ApplyToExecutor(e *Executor) { + e.OutputCIAuto = o.enabled +} + // WithTaskSorter sets the sorter that the [Executor] will use to sort tasks. By // default, the sorter is set to sort tasks alphabetically, but with tasks with // no namespace (in the root Taskfile) first. diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 51bec004..be90ea69 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -71,6 +71,7 @@ var ( Dir string Entrypoint string Output ast.Output + OutputCIAuto bool Color bool Interval time.Duration Failfast bool @@ -143,10 +144,11 @@ func init() { pflag.BoolVarP(&ExitCode, "exit-code", "x", false, "Pass-through the exit code of the task command.") pflag.StringVarP(&Dir, "dir", "d", "", "Sets the directory in which Task will execute and look for a Taskfile.") pflag.StringVarP(&Entrypoint, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`) - pflag.StringVarP(&Output.Name, "output", "o", "", "Sets output style: [interleaved|group|prefixed].") + pflag.StringVarP(&Output.Name, "output", "o", "", "Sets output style: [interleaved|group|prefixed|gitlab].") pflag.StringVar(&Output.Group.Begin, "output-group-begin", "", "Message template to print before a task's grouped output.") pflag.StringVar(&Output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.") pflag.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.") + OutputCIAuto = getConfig(config, "OUTPUT_CI_AUTO", func() *bool { return config.OutputCIAuto }, false) pflag.BoolVarP(&Color, "color", "c", getConfig(config, "COLOR", func() *bool { return config.Color }, true), "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.") pflag.IntVarP(&Concurrency, "concurrency", "C", getConfig(config, "CONCURRENCY", func() *int { return config.Concurrency }, 0), "Limit number of tasks to run concurrently.") pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.") @@ -305,6 +307,7 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) { task.WithConcurrency(Concurrency), task.WithInterval(Interval), task.WithOutputStyle(Output), + task.WithOutputCIAuto(OutputCIAuto), task.WithTaskSorter(sorter), task.WithVersionCheck(true), task.WithFailfast(Failfast), diff --git a/internal/output/gitlab.go b/internal/output/gitlab.go new file mode 100644 index 00000000..030dd3b0 --- /dev/null +++ b/internal/output/gitlab.go @@ -0,0 +1,97 @@ +package output + +import ( + "bytes" + "fmt" + "io" + "regexp" + "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 CI collapsible section markers]: https://docs.gitlab.com/ci/jobs/job_logs/#create-custom-collapsible-sections +type GitLab struct { + Collapsed bool + ErrorOnly bool +} + +func (g GitLab) WrapWriter(stdOut, _ io.Writer, _ string, 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 { + writer io.Writer + buff bytes.Buffer + id string + header string + collapsed bool + startTS int64 +} + +func (gw *gitlabWriter) Write(p []byte) (int, error) { + return gw.buff.Write(p) +} + +func (gw *gitlabWriter) close() error { + 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, "_") +} diff --git a/internal/output/output.go b/internal/output/output.go index 9940f29f..1201f4ee 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -34,6 +34,14 @@ func BuildFor(o *ast.Output, logger *logger.Logger) (Output, error) { 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) } diff --git a/internal/output/output_test.go b/internal/output/output_test.go index ba03c9ad..47d72bad 100644 --- a/internal/output/output_test.go +++ b/internal/output/output_test.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "regexp" "testing" "github.com/fatih/color" @@ -121,6 +122,117 @@ func TestGroupErrorOnlyShowsOutputOnError(t *testing.T) { assert.Equal(t, "std-out\nstd-err\n", b.String()) } +func gitlabTaskCache(taskName string) *templater.Cache { + return &templater.Cache{ + Vars: ast.NewVars( + &ast.VarElement{ + Key: "TASK", + Value: ast.Var{Value: taskName}, + }, + ), + } +} + +var gitlabMarkerPattern = regexp.MustCompile( + `\x1b\[0Ksection_start:(\d+):(\S+?)(\[[^\]]+\])?\r\x1b\[0K(.*)\n` + + `(?s)(.*)` + + `\x1b\[0Ksection_end:(\d+):(\S+)\r\x1b\[0K\n$`, +) + +func TestGitLab(t *testing.T) { + t.Parallel() + + var b bytes.Buffer + o := output.GitLab{} + w, _, cleanup := o.WrapWriter(&b, io.Discard, "", gitlabTaskCache("build")) + + fmt.Fprintln(w, "hello") + assert.Equal(t, "", b.String(), "output must be buffered until close") + require.NoError(t, cleanup(nil)) + + m := gitlabMarkerPattern.FindStringSubmatch(b.String()) + require.NotNil(t, m, "output should match GitLab section markers, got: %q", b.String()) + assert.Equal(t, m[2], m[7], "start and end section IDs must match") + assert.Empty(t, m[3], "collapsed option should not be present by default") + assert.Equal(t, "build", m[4], "section header should be the task name") + assert.Equal(t, "hello\n", m[5], "wrapped content must be preserved") + assert.Contains(t, m[2], "build_", "section ID should be prefixed with slugged task name") +} + +func TestGitLabUniqueSectionIDs(t *testing.T) { + t.Parallel() + + o := output.GitLab{} + + ids := make([]string, 3) + for i := range ids { + var b bytes.Buffer + w, _, cleanup := o.WrapWriter(&b, io.Discard, "", gitlabTaskCache("build")) + fmt.Fprintln(w, "x") + require.NoError(t, cleanup(nil)) + m := gitlabMarkerPattern.FindStringSubmatch(b.String()) + require.NotNil(t, m) + ids[i] = m[2] + } + + assert.NotEqual(t, ids[0], ids[1]) + assert.NotEqual(t, ids[1], ids[2]) + assert.NotEqual(t, ids[0], ids[2]) +} + +func TestGitLabCollapsed(t *testing.T) { + t.Parallel() + + var b bytes.Buffer + o := output.GitLab{Collapsed: true} + w, _, cleanup := o.WrapWriter(&b, io.Discard, "", gitlabTaskCache("build")) + fmt.Fprintln(w, "x") + require.NoError(t, cleanup(nil)) + + m := gitlabMarkerPattern.FindStringSubmatch(b.String()) + require.NotNil(t, m) + assert.Equal(t, "[collapsed=true]", m[3]) +} + +func TestGitLabErrorOnlySwallowsOutputOnNoError(t *testing.T) { + t.Parallel() + + var b bytes.Buffer + o := output.GitLab{ErrorOnly: true} + w, _, cleanup := o.WrapWriter(&b, io.Discard, "", gitlabTaskCache("build")) + fmt.Fprintln(w, "hello") + require.NoError(t, cleanup(nil)) + assert.Empty(t, b.String()) +} + +func TestGitLabErrorOnlyShowsOutputOnError(t *testing.T) { + t.Parallel() + + var b bytes.Buffer + o := output.GitLab{ErrorOnly: true} + w, _, cleanup := o.WrapWriter(&b, io.Discard, "", gitlabTaskCache("build")) + fmt.Fprintln(w, "hello") + require.NoError(t, cleanup(errors.New("boom"))) + + m := gitlabMarkerPattern.FindStringSubmatch(b.String()) + require.NotNil(t, m) + assert.Equal(t, "hello\n", m[5]) +} + +func TestGitLabSlugSanitizesTaskName(t *testing.T) { + t.Parallel() + + var b bytes.Buffer + o := output.GitLab{} + w, _, cleanup := o.WrapWriter(&b, io.Discard, "", gitlabTaskCache("my task:with spaces")) + fmt.Fprintln(w, "x") + require.NoError(t, cleanup(nil)) + + m := gitlabMarkerPattern.FindStringSubmatch(b.String()) + require.NotNil(t, m) + assert.Regexp(t, `^[a-zA-Z0-9_.-]+$`, m[2], "section ID must only contain GitLab-allowed chars") +} + func TestPrefixed(t *testing.T) { //nolint:paralleltest // cannot run in parallel var b bytes.Buffer l := &logger.Logger{ diff --git a/setup.go b/setup.go index d9f57d46..7f2c0bd7 100644 --- a/setup.go +++ b/setup.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "slices" + "strconv" "strings" "sync" @@ -202,12 +203,27 @@ func (e *Executor) setupOutput() error { if !e.OutputStyle.IsSet() { e.OutputStyle = e.Taskfile.Output } + if !e.OutputStyle.IsSet() && e.OutputCIAuto { + if name := detectCIOutput(); name != "" { + e.OutputStyle.Name = name + } + } var err error e.Output, err = output.BuildFor(&e.OutputStyle, e.Logger) return err } +// detectCIOutput returns the name of a CI-aware output style to use based +// on environment variables set by common CI runners. Returns an empty string +// when no supported CI environment is detected. +func detectCIOutput() string { + if isGitLab, _ := strconv.ParseBool(os.Getenv("GITLAB_CI")); isGitLab { + return "gitlab" + } + return "" +} + func (e *Executor) setupCompiler() error { if e.UserWorkingDir == "" { var err error diff --git a/setup_test.go b/setup_test.go new file mode 100644 index 00000000..266605b0 --- /dev/null +++ b/setup_test.go @@ -0,0 +1,97 @@ +package task + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/go-task/task/v3/internal/logger" + "github.com/go-task/task/v3/taskfile/ast" +) + +func TestDetectCIOutput(t *testing.T) { + cases := []struct { + name string + env map[string]string + want string + }{ + {name: "no CI detected", env: nil, want: ""}, + {name: "GITLAB_CI=true", env: map[string]string{"GITLAB_CI": "true"}, want: "gitlab"}, + {name: "GITLAB_CI=1", env: map[string]string{"GITLAB_CI": "1"}, want: "gitlab"}, + {name: "GITLAB_CI=false", env: map[string]string{"GITLAB_CI": "false"}, want: ""}, + {name: "GITLAB_CI empty", env: map[string]string{"GITLAB_CI": ""}, want: ""}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("GITLAB_CI", "") // reset + for k, v := range tc.env { + t.Setenv(k, v) + } + assert.Equal(t, tc.want, detectCIOutput()) + }) + } +} + +func TestSetupOutputPriority(t *testing.T) { + cases := []struct { + name string + cliStyle ast.Output + taskfileStyle ast.Output + ciAuto bool + gitlabEnv string + wantName string + }{ + { + name: "CLI wins over everything", + cliStyle: ast.Output{Name: "prefixed"}, + taskfileStyle: ast.Output{Name: "group", Group: ast.OutputGroup{ + Begin: "b", End: "e", + }}, + ciAuto: true, + gitlabEnv: "true", + wantName: "prefixed", + }, + { + name: "Taskfile wins over auto-detect", + taskfileStyle: ast.Output{Name: "prefixed"}, + ciAuto: true, + gitlabEnv: "true", + wantName: "prefixed", + }, + { + name: "auto-detect activates when nothing explicit", + ciAuto: true, + gitlabEnv: "true", + wantName: "gitlab", + }, + { + name: "auto-detect disabled does nothing", + ciAuto: false, + gitlabEnv: "true", + wantName: "", + }, + { + name: "auto-detect without CI env does nothing", + ciAuto: true, + gitlabEnv: "", + wantName: "", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("GITLAB_CI", tc.gitlabEnv) + + e := &Executor{ + OutputStyle: tc.cliStyle, + OutputCIAuto: tc.ciAuto, + Taskfile: &ast.Taskfile{Output: tc.taskfileStyle}, + Logger: &logger.Logger{}, + } + require.NoError(t, e.setupOutput()) + assert.Equal(t, tc.wantName, e.OutputStyle.Name) + }) + } +} diff --git a/taskfile/ast/output.go b/taskfile/ast/output.go index 29ec58f5..a7f3897b 100644 --- a/taskfile/ast/output.go +++ b/taskfile/ast/output.go @@ -12,6 +12,8 @@ type Output struct { Name string `yaml:"-"` // Group specific style Group OutputGroup + // GitLab specific style + GitLab OutputGitLab } // IsSet returns true if and only if a custom output style is set. @@ -32,19 +34,30 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error { case yaml.MappingNode: var tmp struct { - Group *OutputGroup + Group *OutputGroup + GitLab *OutputGitLab `yaml:"gitlab"` } if err := node.Decode(&tmp); err != nil { return errors.NewTaskfileDecodeError(err, node) } - if tmp.Group == nil { - return errors.NewTaskfileDecodeError(nil, node).WithMessage(`output style must have the "group" key when in mapping form`) + switch { + case tmp.Group != nil && tmp.GitLab != nil: + return errors.NewTaskfileDecodeError(nil, node).WithMessage(`output style cannot set both "group" and "gitlab"`) + case tmp.Group != nil: + *s = Output{ + Name: "group", + Group: *tmp.Group, + } + return nil + case tmp.GitLab != nil: + *s = Output{ + Name: "gitlab", + GitLab: *tmp.GitLab, + } + return nil + default: + return errors.NewTaskfileDecodeError(nil, node).WithMessage(`output style must have the "group" or "gitlab" key when in mapping form`) } - *s = Output{ - Name: "group", - Group: *tmp.Group, - } - return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("output") @@ -63,3 +76,9 @@ func (g *OutputGroup) IsSet() bool { } return g.Begin != "" || g.End != "" } + +// OutputGitLab is the style options specific to the GitLab style. +type OutputGitLab struct { + Collapsed bool + ErrorOnly bool `yaml:"error_only"` +} diff --git a/taskrc/ast/taskrc.go b/taskrc/ast/taskrc.go index d1f5d3a4..639dcffc 100644 --- a/taskrc/ast/taskrc.go +++ b/taskrc/ast/taskrc.go @@ -17,6 +17,7 @@ type TaskRC struct { DisableFuzzy *bool `yaml:"disable-fuzzy"` Concurrency *int `yaml:"concurrency"` Interactive *bool `yaml:"interactive"` + OutputCIAuto *bool `yaml:"output-ci-auto"` Remote Remote `yaml:"remote"` Failfast bool `yaml:"failfast"` Experiments map[string]int `yaml:"experiments"` @@ -69,5 +70,6 @@ func (t *TaskRC) Merge(other *TaskRC) { t.DisableFuzzy = cmp.Or(other.DisableFuzzy, t.DisableFuzzy) t.Concurrency = cmp.Or(other.Concurrency, t.Concurrency) t.Interactive = cmp.Or(other.Interactive, t.Interactive) + t.OutputCIAuto = cmp.Or(other.OutputCIAuto, t.OutputCIAuto) t.Failfast = cmp.Or(other.Failfast, t.Failfast) } diff --git a/taskrc/taskrc_test.go b/taskrc/taskrc_test.go index 902f65de..132d9164 100644 --- a/taskrc/taskrc_test.go +++ b/taskrc/taskrc_test.go @@ -306,4 +306,27 @@ remote: assert.Equal(t, &cacheExpiry, base.Remote.CacheExpiry) assert.Equal(t, []string{"github.com", "gitlab.com"}, base.Remote.TrustedHosts) }) + + t.Run("output-ci-auto merge", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel + trueVal := true + falseVal := false + + t.Run("other overrides nil base", func(t *testing.T) { //nolint:paralleltest + base := &ast.TaskRC{} + base.Merge(&ast.TaskRC{OutputCIAuto: &trueVal}) + assert.Equal(t, &trueVal, base.OutputCIAuto) + }) + + t.Run("other overrides base", func(t *testing.T) { //nolint:paralleltest + base := &ast.TaskRC{OutputCIAuto: &falseVal} + base.Merge(&ast.TaskRC{OutputCIAuto: &trueVal}) + assert.Equal(t, &trueVal, base.OutputCIAuto) + }) + + t.Run("nil other does not override base", func(t *testing.T) { //nolint:paralleltest + base := &ast.TaskRC{OutputCIAuto: &trueVal} + base.Merge(&ast.TaskRC{}) + assert.Equal(t, &trueVal, base.OutputCIAuto) + }) + }) } diff --git a/website/src/docs/guide.md b/website/src/docs/guide.md index 9463cb44..fb567703 100644 --- a/website/src/docs/guide.md +++ b/website/src/docs/guide.md @@ -2426,12 +2426,13 @@ the shell in real-time. This is good for having live feedback for logging printed by commands, but the output can become messy if you have multiple commands running simultaneously and printing lots of stuff. -To make this more customizable, there are currently three different output +To make this more customizable, there are currently four different output options you can choose: - `interleaved` (default) - `group` - `prefixed` +- `gitlab` To choose another one, just set it to root in the Taskfile: @@ -2535,6 +2536,44 @@ $ task default [print-baz] baz ``` +The `gitlab` output wraps each task's output in +[GitLab CI collapsible section markers](https://docs.gitlab.com/ci/jobs/job_logs/#create-custom-collapsible-sections). +Section names are generated automatically so that start and end markers always +match and stay unique per invocation — even when the same task runs multiple +times in the same job. + +```yaml +version: '3' + +output: gitlab +``` + +Two options are available: + +- `collapsed`: maps to GitLab's native + [`[collapsed=true]`](https://docs.gitlab.com/ci/jobs/job_logs/#create-custom-collapsible-sections) + option, which tells GitLab to fold the section by default in the UI. +- `error_only`: a Task-level option (same as in the [`group`](#output-syntax) + style) that swallows the command output — markers included — for tasks that + exit with a zero status code. + +```yaml +version: '3' + +output: + gitlab: + collapsed: true + error_only: true +``` + +::: tip + +Rather than hard-coding `output: gitlab` in your Taskfile (which also affects +local development), consider using [`output-ci-auto`](#automatic-ci-output) so +the mode is only activated in CI. + +::: + ::: tip The `output` option can also be specified by the `--output` or `-o` flags. @@ -2563,6 +2602,28 @@ summary, making it easier to spot failures without scrolling through logs. This feature requires no configuration and works automatically. +### Automatic CI output + +When `output-ci-auto: true` is set in a [`.taskrc.yml`](./taskrc.md) file, Task +will automatically select a CI-aware [output style](#output-syntax) based on +the environment it is running in, but only when no output style is configured +explicitly (via the Taskfile, `--output`, or `TASK_X_OUTPUT`). + +Currently supported: + +| Environment variable | Output style selected | +| -------------------- | --------------------- | +| `GITLAB_CI=true` | `gitlab` | + +This lets you keep your Taskfile neutral — local developers get the default +`interleaved` output, while CI runs get their matching CI-flavored output +without any per-job configuration. + +```yaml +# .taskrc.yml +output-ci-auto: true +``` + ## Interactive CLI application When running interactive CLI applications inside Task they can sometimes behave diff --git a/website/src/docs/reference/cli.md b/website/src/docs/reference/cli.md index aeb417f1..6fdbb807 100644 --- a/website/src/docs/reference/cli.md +++ b/website/src/docs/reference/cli.md @@ -224,7 +224,8 @@ task backup --global #### `-o, --output ` -Set output style. Available modes: `interleaved`, `group`, `prefixed`. +Set output style. Available modes: `interleaved`, `group`, `prefixed`, +`gitlab`. ```bash task test --output group diff --git a/website/src/docs/reference/config.md b/website/src/docs/reference/config.md index 62d12bc9..1369e1b1 100644 --- a/website/src/docs/reference/config.md +++ b/website/src/docs/reference/config.md @@ -166,6 +166,21 @@ failfast: true interactive: true ``` +### `output-ci-auto` + +- **Type**: `boolean` +- **Default**: `false` +- **Description**: Automatically select a CI-aware + [output style](../guide.md#output-syntax) when a supported CI environment + is detected and no output style is explicitly configured (via the Taskfile + or `--output`). Currently maps `GITLAB_CI=true` to the `gitlab` output + style. +- **Environment variable**: [`TASK_OUTPUT_CI_AUTO`](./environment.md#task-output-ci-auto) + +```yaml +output-ci-auto: true +``` + ## Example Configuration Here's a complete example of a `.taskrc.yml` file with all available options: diff --git a/website/src/docs/reference/environment.md b/website/src/docs/reference/environment.md index 45937e92..0b072c4d 100644 --- a/website/src/docs/reference/environment.md +++ b/website/src/docs/reference/environment.md @@ -81,6 +81,16 @@ variables. The priority order is: CLI flags > environment variables > config fil - **Default**: `false` - **Description**: Prompt for missing required variables +### `TASK_OUTPUT_CI_AUTO` + +- **Type**: `boolean` (`true`, `false`, `1`, `0`) +- **Default**: `false` +- **Description**: Automatically select a CI-aware output style when a + supported CI environment is detected and no output style is explicitly + configured. See [output syntax](../guide.md#output-syntax) and + [automatic CI output](../guide.md#automatic-ci-output). +- **Config equivalent**: [`output-ci-auto`](./config.md#output-ci-auto) + ### `TASK_TEMP_DIR` Defines the location of Task's temporary directory which is used for storing diff --git a/website/src/docs/reference/schema.md b/website/src/docs/reference/schema.md index 2aa51eb3..ccc6036b 100644 --- a/website/src/docs/reference/schema.md +++ b/website/src/docs/reference/schema.md @@ -29,7 +29,7 @@ version: '3' - **Type**: `string` or `object` - **Default**: `interleaved` -- **Options**: `interleaved`, `group`, `prefixed` +- **Options**: `interleaved`, `group`, `prefixed`, `gitlab` - **Description**: Controls how task output is displayed ```yaml diff --git a/website/src/public/schema.json b/website/src/public/schema.json index 2210952d..cd8dddd4 100644 --- a/website/src/public/schema.json +++ b/website/src/public/schema.json @@ -595,7 +595,7 @@ }, "outputString": { "type": "string", - "enum": ["interleaved", "prefixed", "group"], + "enum": ["interleaved", "prefixed", "group", "gitlab"], "default": "interleaved" }, "outputObject": { @@ -616,6 +616,22 @@ "default": false } } + }, + "gitlab": { + "type": "object", + "description": "Wraps each task's output in GitLab CI collapsible section markers", + "properties": { + "collapsed": { + "description": "Passes the native GitLab [collapsed=true] option so sections are folded by default in the GitLab CI UI", + "type": "boolean", + "default": false + }, + "error_only": { + "description": "Swallows command output on zero exit code (Task-level behavior, identical to group.error_only)", + "type": "boolean", + "default": false + } + } } }, "additionalProperties": false