From 6660afc8d2ce1dadfcbb9a02a4a4e72d413c89b9 Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Thu, 18 Dec 2025 08:40:37 +0100 Subject: [PATCH] feat: auto-detect color output in CI environments (#2569) --- CHANGELOG.md | 5 +++++ internal/flags/flags.go | 27 ++++++++++++++++++++++++++- internal/logger/logger.go | 5 ----- taskrc/ast/taskrc.go | 2 ++ website/src/docs/reference/config.md | 12 ++++++++++++ website/src/public/schema-taskrc.json | 4 ++++ 6 files changed, 49 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8559f6fc..ac056445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,11 @@ customize the cache directory for Remote Taskfiles (#2572 by @vmaerten). - Zsh completion now supports zstyle verbose option to show or hide task descriptions (#2571 by @vmaerten). +- Task now automatically enables colored output in CI environments (GitHub + Actions, GitLab CI, etc.) without requiring FORCE_COLOR=1 (#2569 by + @vmaerten). +- Added color taskrc option to explicitly enable or disable colored output + globally (#2569 by @vmaerten). ## v3.45.5 - 2025-11-11 diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 656e250f..6019e51d 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -5,8 +5,10 @@ import ( "log" "os" "path/filepath" + "strconv" "time" + "github.com/fatih/color" "github.com/spf13/pflag" "github.com/go-task/task/v3" @@ -140,7 +142,7 @@ func init() { 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.") - pflag.BoolVarP(&Color, "color", "c", true, "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.") + pflag.BoolVarP(&Color, "color", "c", getConfig(config, 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, func() *int { return config.Concurrency }, 0), "Limit number of tasks to run concurrently.") pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.") pflag.BoolVarP(&Failfast, "failfast", "F", getConfig(config, func() *bool { return &config.Failfast }, false), "When running tasks in parallel, stop all tasks if one fails.") @@ -166,6 +168,29 @@ func init() { pflag.StringVar(&RemoteCacheDir, "remote-cache-dir", getConfig(config, func() *string { return config.Remote.CacheDir }, env.GetTaskEnv("REMOTE_DIR")), "Directory to cache remote Taskfiles.") } pflag.Parse() + + // Auto-detect color based on environment when not explicitly configured + // Priority: CLI flag > taskrc config > NO_COLOR > FORCE_COLOR/CI > default + colorExplicitlySet := pflag.Lookup("color").Changed || (config != nil && config.Color != nil) + if !colorExplicitlySet { + if os.Getenv("NO_COLOR") != "" { + Color = false + color.NoColor = true + } else if os.Getenv("FORCE_COLOR") != "" || isCI() { + Color = true + color.NoColor = false // Force colors even without TTY + } + // Otherwise, let fatih/color auto-detect TTY + } else { + // Explicit config: sync with fatih/color + color.NoColor = !Color + } +} + +// isCI returns true if running in a CI environment +func isCI() bool { + ci, _ := strconv.ParseBool(os.Getenv("CI")) + return ci } func Validate() error { diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 874c197f..6ebcf7d9 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -3,7 +3,6 @@ package logger import ( "bufio" "io" - "os" "slices" "strconv" "strings" @@ -96,10 +95,6 @@ func BrightRed() PrintFunc { } func envColor(name string, defaultColor color.Attribute) []color.Attribute { - if os.Getenv("FORCE_COLOR") != "" { - color.NoColor = false - } - // Fetch the environment variable override := env.GetTaskEnv(name) diff --git a/taskrc/ast/taskrc.go b/taskrc/ast/taskrc.go index 5e9acbb0..12f5524a 100644 --- a/taskrc/ast/taskrc.go +++ b/taskrc/ast/taskrc.go @@ -12,6 +12,7 @@ import ( type TaskRC struct { Version *semver.Version `yaml:"version"` Verbose *bool `yaml:"verbose"` + Color *bool `yaml:"color"` DisableFuzzy *bool `yaml:"disable-fuzzy"` Concurrency *int `yaml:"concurrency"` Remote Remote `yaml:"remote"` @@ -55,6 +56,7 @@ func (t *TaskRC) Merge(other *TaskRC) { } t.Verbose = cmp.Or(other.Verbose, t.Verbose) + t.Color = cmp.Or(other.Color, t.Color) t.DisableFuzzy = cmp.Or(other.DisableFuzzy, t.DisableFuzzy) t.Concurrency = cmp.Or(other.Concurrency, t.Concurrency) t.Failfast = cmp.Or(other.Failfast, t.Failfast) diff --git a/website/src/docs/reference/config.md b/website/src/docs/reference/config.md index a0129e8f..04a33a5d 100644 --- a/website/src/docs/reference/config.md +++ b/website/src/docs/reference/config.md @@ -91,6 +91,17 @@ experiments: verbose: true ``` +### `color` + +- **Type**: `boolean` +- **Default**: `true` +- **Description**: Enable colored output. Colors are automatically enabled in CI environments (`CI=true`). +- **CLI equivalent**: [`-c, --color`](./cli.md#-c---color) + +```yaml +color: false +``` + ### `disable-fuzzy` - **Type**: `boolean` @@ -131,6 +142,7 @@ Here's a complete example of a `.taskrc.yml` file with all available options: ```yaml # Global settings verbose: true +color: true disable-fuzzy: false concurrency: 2 diff --git a/website/src/public/schema-taskrc.json b/website/src/public/schema-taskrc.json index 34c6ed53..92fed01c 100644 --- a/website/src/public/schema-taskrc.json +++ b/website/src/public/schema-taskrc.json @@ -57,6 +57,10 @@ "type": "boolean", "description": "Enable verbose output" }, + "color": { + "type": "boolean", + "description": "Enable colored output" + }, "disable-fuzzy": { "type": "boolean", "description": "Disable fuzzy matching for task names"