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