diff --git a/experiments/experiments.go b/experiments/experiments.go index f4deeb3415..24bd16b1df 100644 --- a/experiments/experiments.go +++ b/experiments/experiments.go @@ -9,6 +9,7 @@ import ( "github.com/joho/godotenv" "github.com/go-task/task/v3/taskrc" + "github.com/go-task/task/v3/taskrc/ast" ) const envPrefix = "TASK_X_" @@ -31,11 +32,15 @@ var ( var xList []Experiment func Parse(dir string) { + config, _ := taskrc.GetConfig(dir) + + ParseWithConfig(dir, config) +} + +func ParseWithConfig(dir string, config *ast.TaskRC) { // Read any .env files readDotEnv(dir) - config, _ := taskrc.GetConfig(dir) - // Initialize the experiments GentleForce = New("GENTLE_FORCE", config, 1) RemoteTaskfiles = New("REMOTE_TASKFILES", config, 1) diff --git a/internal/flags/flags.go b/internal/flags/flags.go index dab9fdf8f4..f76620adb7 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -5,7 +5,6 @@ import ( "log" "os" "path/filepath" - "strconv" "time" "github.com/spf13/pflag" @@ -13,9 +12,10 @@ import ( "github.com/go-task/task/v3" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/experiments" - "github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/taskfile/ast" + "github.com/go-task/task/v3/taskrc" + taskrcast "github.com/go-task/task/v3/taskrc/ast" ) const usage = `Usage: task [flags...] [task...] @@ -95,7 +95,9 @@ func init() { // Parse the experiments dir = cmp.Or(dir, filepath.Dir(entrypoint)) - experiments.Parse(dir) + + config, _ := taskrc.GetConfig(dir) + experiments.ParseWithConfig(dir, config) // Parse the rest of the flags log.SetFlags(0) @@ -104,10 +106,7 @@ func init() { log.Print(usage) pflag.PrintDefaults() } - offline, err := strconv.ParseBool(cmp.Or(env.GetTaskEnv("OFFLINE"), "false")) - if err != nil { - offline = false - } + pflag.BoolVar(&Version, "version", false, "Show Task version.") pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.") pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.") @@ -118,9 +117,9 @@ func init() { pflag.StringVar(&TaskSort, "sort", "", "Changes the order of the tasks when listed. [default|alphanumeric|none].") pflag.BoolVar(&Status, "status", false, "Exits with non-zero exit code if any of the given tasks is not up-to-date.") pflag.BoolVar(&NoStatus, "no-status", false, "Ignore status when listing tasks as JSON") - pflag.BoolVar(&Insecure, "insecure", false, "Forces Task to download Taskfiles over insecure connections.") + pflag.BoolVar(&Insecure, "insecure", getConfig(config, config.Remote.Insecure, false), "Forces Task to download Taskfiles over insecure connections.") pflag.BoolVarP(&Watch, "watch", "w", false, "Enables watch of the given task.") - pflag.BoolVarP(&Verbose, "verbose", "v", false, "Enables verbose mode.") + pflag.BoolVarP(&Verbose, "verbose", "v", getConfig(config, config.Verbose, false), "Enables verbose mode.") pflag.BoolVarP(&Silent, "silent", "s", false, "Disables echoing.") pflag.BoolVarP(&AssumeYes, "yes", "y", false, "Assume \"yes\" as answer to all prompts.") pflag.BoolVarP(&Parallel, "parallel", "p", false, "Executes tasks provided on command line in parallel.") @@ -134,7 +133,7 @@ func init() { 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.IntVarP(&Concurrency, "concurrency", "C", 0, "Limit number of tasks to run concurrently.") + pflag.IntVarP(&Concurrency, "concurrency", "C", getConfig(config, config.Concurrency, 0), "Limit number of tasks to run concurrently.") pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.") pflag.BoolVarP(&Global, "global", "g", false, "Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.") pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.") @@ -150,12 +149,11 @@ func init() { // Remote Taskfiles experiment will adds the "download" and "offline" flags if experiments.RemoteTaskfiles.Enabled() { pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.") - pflag.BoolVar(&Offline, "offline", offline, "Forces Task to only use local or cached Taskfiles.") - pflag.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.") + pflag.BoolVar(&Offline, "offline", getConfig(config, config.Remote.Offline, false), "Forces Task to only use local or cached Taskfiles.") + pflag.DurationVar(&Timeout, "timeout", getConfig(config, config.Remote.Timeout, time.Second*10), "Timeout for downloading remote Taskfiles.") pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.") - pflag.DurationVar(&CacheExpiryDuration, "expiry", 0, "Expiry duration for cached remote Taskfiles.") + pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, config.Remote.Timeout, 0), "Expiry duration for cached remote Taskfiles.") } - pflag.Parse() } @@ -251,3 +249,15 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) { task.WithVersionCheck(true), ) } + +// getConfig extracts a config value directly from a pointer field with a fallback default +func getConfig[T any](config *taskrcast.TaskRC, field *T, fallback T) T { + if config == nil { + return fallback + } + + if field != nil { + return *field + } + return fallback +} diff --git a/taskrc/ast/taskrc.go b/taskrc/ast/taskrc.go index 13972090a1..9410ed1738 100644 --- a/taskrc/ast/taskrc.go +++ b/taskrc/ast/taskrc.go @@ -1,27 +1,48 @@ package ast import ( + "cmp" "maps" + "time" "github.com/Masterminds/semver/v3" ) type TaskRC struct { Version *semver.Version `yaml:"version"` + Verbose *bool `yaml:"verbose"` + Concurrency *int `yaml:"concurrency"` + Remote Remote `yaml:"remote"` Experiments map[string]int `yaml:"experiments"` } +type Remote struct { + Insecure *bool `yaml:"insecure"` + Offline *bool `yaml:"offline"` + Timeout *time.Duration `yaml:"timeout"` + CacheExpiry *time.Duration `yaml:"cache-expiry"` +} + // Merge combines the current TaskRC with another TaskRC, prioritizing non-nil fields from the other TaskRC. func (t *TaskRC) Merge(other *TaskRC) { if other == nil { return } - if t.Version == nil && other.Version != nil { - t.Version = other.Version - } + + t.Version = cmp.Or(other.Version, t.Version) + if t.Experiments == nil && other.Experiments != nil { t.Experiments = other.Experiments } else if t.Experiments != nil && other.Experiments != nil { maps.Copy(t.Experiments, other.Experiments) } + + // Merge Remote fields + t.Remote.Insecure = cmp.Or(other.Remote.Insecure, t.Remote.Insecure) + t.Remote.Offline = cmp.Or(other.Remote.Offline, t.Remote.Offline) + t.Remote.Timeout = cmp.Or(other.Remote.Timeout, t.Remote.Timeout) + t.Remote.CacheExpiry = cmp.Or(other.Remote.CacheExpiry, t.Remote.CacheExpiry) + + t.Verbose = cmp.Or(other.Verbose, t.Verbose) + t.Concurrency = cmp.Or(other.Concurrency, t.Concurrency) } diff --git a/website/.vitepress/config.ts b/website/.vitepress/config.ts index 5386ad49df..d293b58eef 100644 --- a/website/.vitepress/config.ts +++ b/website/.vitepress/config.ts @@ -5,7 +5,8 @@ import { resolve } from 'path'; import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'; import { groupIconMdPlugin, - groupIconVitePlugin + groupIconVitePlugin, + localIconLoader } from 'vitepress-plugin-group-icons'; import { team } from './team.ts'; import { ogUrl, taskDescription, taskName } from './meta.ts'; @@ -107,7 +108,20 @@ export default defineConfig({ } }, vite: { - plugins: [groupIconVitePlugin()], + plugins: [ + groupIconVitePlugin({ + customIcon: { + '.taskrc.yml': localIconLoader( + import.meta.url, + './theme/icons/task.svg' + ), + 'Taskfile.yml': localIconLoader( + import.meta.url, + './theme/icons/task.svg' + ) + } + }) + ], resolve: { alias: [ { diff --git a/website/.vitepress/theme/icons/task.svg b/website/.vitepress/theme/icons/task.svg new file mode 100644 index 0000000000..adccbc78d5 --- /dev/null +++ b/website/.vitepress/theme/icons/task.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/src/docs/experiments/remote-taskfiles.md b/website/src/docs/experiments/remote-taskfiles.md index afb3f36a20..11e62f96b3 100644 --- a/website/src/docs/experiments/remote-taskfiles.md +++ b/website/src/docs/experiments/remote-taskfiles.md @@ -290,3 +290,64 @@ You can force Task to ignore the cache and download the latest version by using the `--download` flag. You can use the `--clear-cache` flag to clear all cached remote files. + +## Configuration +This experiment adds a new `remote` section to the [configuration file](../reference/config.md). + +- **Type**: `object` +- **Description**: Remote configuration settings for handling remote Taskfiles + +```yaml +remote: + insecure: false + offline: false + timeout: "30s" + cache-expiry: "24h" +``` + +#### `insecure` + +- **Type**: `boolean` +- **Default**: `false` +- **Description**: Allow insecure connections when fetching remote Taskfiles + +```yaml +remote: + insecure: true +``` + +#### `offline` + +- **Type**: `boolean` +- **Default**: `false` +- **Description**: Work in offline mode, preventing remote Taskfile fetching + +```yaml +remote: + offline: true +``` + +#### `timeout` + +- **Type**: `string` +- **Default**: Not specified +- **Pattern**: `^[0-9]+(ns|us|µs|ms|s|m|h)$` +- **Description**: Timeout duration for remote operations (e.g., '30s', '5m') + +```yaml +remote: + timeout: "1m" +``` + +#### `cache-expiry` + +- **Type**: `string` +- **Default**: Not specified +- **Pattern**: `^[0-9]+(ns|us|µs|ms|s|m|h)$` +- **Description**: Cache expiry duration for remote Taskfiles (e.g., '1h', '24h') + +```yaml +remote: + cache-expiry: "6h" +``` + diff --git a/website/src/docs/reference/config.md b/website/src/docs/reference/config.md index ddba3b4c39..790d5d4e7c 100644 --- a/website/src/docs/reference/config.md +++ b/website/src/docs/reference/config.md @@ -68,3 +68,44 @@ option_3: foo # Taken from $XDG_CONFIG_HOME/task/.taskrc.yml The experiments section allows you to enable Task's experimental features. These options are not enumerated here. Instead, please refer to our [experiments documentation](../experiments/index.md) for more information. + +```yaml +experiments: + feature_name: 1 + another_feature: 2 +``` + +### `verbose` + +- **Type**: `boolean` +- **Default**: `false` +- **Description**: Enable verbose output for all tasks +- **CLI equivalent**: [`-v, --verbose`](./cli.md#-v---verbose) + +```yaml +verbose: true +``` + +### `concurrency` + +- **Type**: `integer` +- **Minimum**: `1` +- **Description**: Number of concurrent tasks to run +- **CLI equivalent**: [`-C, --concurrency`](./cli.md#-c---concurrency-number) + +```yaml +concurrency: 4 +``` + +## Example Configuration + +Here's a complete example of a `.taskrc.yml` file with all available options: + +```yaml +# Global settings +verbose: true +concurrency: 2 + +# Enable experimental features +experiments: + REMOTE_TASKFILES: 1 diff --git a/website/src/public/schema-taskrc.json b/website/src/public/schema-taskrc.json index 1b5b1fc231..ac101acfb8 100644 --- a/website/src/public/schema-taskrc.json +++ b/website/src/public/schema-taskrc.json @@ -20,6 +20,40 @@ "enum": [0, 1] } } + }, + "remote": { + "type": "object", + "description": "Remote configuration settings", + "properties": { + "insecure": { + "type": "boolean", + "description": "Forces Task to download Taskfiles over insecure connections." + }, + "offline": { + "type": "boolean", + "description": "Forces Task to only use local or cached Taskfiles." + }, + "timeout": { + "type": "string", + "description": "Timeout for downloading remote Taskfiles (e.g., '30s', '5m')", + "pattern": "^[0-9]+(ns|us|µs|ms|s|m|h)$" + }, + "cache-expiry": { + "type": "string", + "description": "Expiry duration for cached remote Taskfiles (e.g., '1h', '24h')", + "pattern": "^[0-9]+(ns|us|µs|ms|s|m|h)$" + } + }, + "additionalProperties": false + }, + "verbose": { + "type": "boolean", + "description": "Enable verbose output" + }, + "concurrency": { + "type": "integer", + "description": "Number of concurrent tasks to run", + "minimum": 1 } }, "additionalProperties": false