feat: XDG taskrc config (#2380)

Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
This commit is contained in:
Pete Davison
2025-08-18 21:43:36 +01:00
committed by GitHub
parent c903d07332
commit f89c12ddf0
14 changed files with 564 additions and 109 deletions

View File

@@ -1,8 +1,27 @@
package ast
import "github.com/Masterminds/semver/v3"
import (
"maps"
"github.com/Masterminds/semver/v3"
)
type TaskRC struct {
Version *semver.Version `yaml:"version"`
Experiments map[string]int `yaml:"experiments"`
}
// 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
}
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)
}
}

View File

@@ -1,10 +1,11 @@
package taskrc
import "github.com/go-task/task/v3/internal/fsext"
import (
"github.com/go-task/task/v3/internal/fsext"
)
type Node struct {
entrypoint string
dir string
}
func NewNode(
@@ -12,13 +13,11 @@ func NewNode(
dir string,
) (*Node, error) {
dir = fsext.DefaultDir(entrypoint, dir)
var err error
entrypoint, dir, err = fsext.Search(entrypoint, dir, defaultTaskRCs)
resolvedEntrypoint, err := fsext.SearchPath(dir, defaultTaskRCs)
if err != nil {
return nil, err
}
return &Node{
entrypoint: entrypoint,
dir: dir,
entrypoint: resolvedEntrypoint,
}, nil
}

View File

@@ -1,6 +1,63 @@
package taskrc
import (
"os"
"path/filepath"
"slices"
"github.com/go-task/task/v3/internal/fsext"
"github.com/go-task/task/v3/taskrc/ast"
)
var defaultTaskRCs = []string{
".taskrc.yml",
".taskrc.yaml",
}
// GetConfig loads and merges local and global Task configuration files
func GetConfig(dir string) (*ast.TaskRC, error) {
var config *ast.TaskRC
reader := NewReader()
// Read the XDG config file
if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
xdgConfigNode, err := NewNode("", filepath.Join(xdgConfigHome, "task"))
if err == nil && xdgConfigNode != nil {
config, err = reader.Read(xdgConfigNode)
if err != nil {
return nil, err
}
}
}
// Find all the nodes from the given directory up to the users home directory
entrypoints, err := fsext.SearchAll("", dir, defaultTaskRCs)
if err != nil {
return nil, err
}
// Reverse the entrypoints since we want the child files to override parent ones
slices.Reverse(entrypoints)
// Loop over the nodes, and merge them into the main config
for _, entrypoint := range entrypoints {
node, err := NewNode("", entrypoint)
if err != nil {
return nil, err
}
localConfig, err := reader.Read(node)
if err != nil {
return nil, err
}
if localConfig == nil {
continue
}
if config == nil {
config = localConfig
continue
}
config.Merge(localConfig)
}
return config, nil
}

137
taskrc/taskrc_test.go Normal file
View File

@@ -0,0 +1,137 @@
package taskrc
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-task/task/v3/taskrc/ast"
)
const (
xdgConfigYAML = `
experiments:
FOO: 1
BAR: 1
BAZ: 1
`
homeConfigYAML = `
experiments:
FOO: 2
BAR: 2
`
localConfigYAML = `
experiments:
FOO: 3
`
)
func setupDirs(t *testing.T) (string, string, string) {
t.Helper()
xdgConfigDir := t.TempDir()
xdgTaskConfigDir := filepath.Join(xdgConfigDir, "task")
require.NoError(t, os.Mkdir(xdgTaskConfigDir, 0o755))
homeDir := t.TempDir()
localDir := filepath.Join(homeDir, "local")
require.NoError(t, os.Mkdir(localDir, 0o755))
t.Setenv("XDG_CONFIG_HOME", xdgConfigDir)
t.Setenv("HOME", homeDir)
return xdgTaskConfigDir, homeDir, localDir
}
func writeFile(t *testing.T, dir, filename, content string) {
t.Helper()
err := os.WriteFile(filepath.Join(dir, filename), []byte(content), 0o644)
assert.NoError(t, err)
}
func TestGetConfig_NoConfigFiles(t *testing.T) { //nolint:paralleltest // cannot run in parallel
_, _, localDir := setupDirs(t)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.Nil(t, cfg)
}
func TestGetConfig_OnlyXDG(t *testing.T) { //nolint:paralleltest // cannot run in parallel
xdgDir, _, localDir := setupDirs(t)
writeFile(t, xdgDir, ".taskrc.yml", xdgConfigYAML)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.Equal(t, &ast.TaskRC{
Version: nil,
Experiments: map[string]int{
"FOO": 1,
"BAR": 1,
"BAZ": 1,
},
}, cfg)
}
func TestGetConfig_OnlyHome(t *testing.T) { //nolint:paralleltest // cannot run in parallel
_, homeDir, localDir := setupDirs(t)
writeFile(t, homeDir, ".taskrc.yml", homeConfigYAML)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.Equal(t, &ast.TaskRC{
Version: nil,
Experiments: map[string]int{
"FOO": 2,
"BAR": 2,
},
}, cfg)
}
func TestGetConfig_OnlyLocal(t *testing.T) { //nolint:paralleltest // cannot run in parallel
_, _, localDir := setupDirs(t)
writeFile(t, localDir, ".taskrc.yml", localConfigYAML)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.Equal(t, &ast.TaskRC{
Version: nil,
Experiments: map[string]int{
"FOO": 3,
},
}, cfg)
}
func TestGetConfig_All(t *testing.T) { //nolint:paralleltest // cannot run in parallel
xdgConfigDir, homeDir, localDir := setupDirs(t)
// Write local config
writeFile(t, localDir, ".taskrc.yml", localConfigYAML)
// Write home config
writeFile(t, homeDir, ".taskrc.yml", homeConfigYAML)
// Write XDG config
writeFile(t, xdgConfigDir, ".taskrc.yml", xdgConfigYAML)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.NotNil(t, cfg)
assert.Equal(t, &ast.TaskRC{
Version: nil,
Experiments: map[string]int{
"FOO": 3,
"BAR": 2,
"BAZ": 1,
},
}, cfg)
}