Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1289,3 +1289,36 @@ func TestIf(t *testing.T) {
NewExecutorTest(t, opts...)
}
}

func TestAutomaticOverride(t *testing.T) {
t.Parallel()
const dir = "testdata/override"
NewExecutorTest(t,
WithName("discovered"),
WithExecutorOptions(task.WithDir(dir), task.WithSilent(true)),
WithTask("dev"),
)
NewExecutorTest(t,
WithName("added task location"),
WithExecutorOptions(task.WithDir(dir), task.WithSilent(true)),
WithTask("added"),
WithFixtureTemplating(),
)
// Only the discovered root's directory is searched, so the invalid
// override in child/ is ignored.
NewExecutorTest(t,
WithName("subdirectory"),
WithExecutorOptions(task.WithDir(dir+"/child"), task.WithSilent(true)),
WithTask("dev"),
)
NewExecutorTest(t,
WithName("explicit file"),
WithExecutorOptions(task.WithDir(dir), task.WithEntrypoint(dir+"/Taskfile.yml"), task.WithSilent(true)),
WithTask("dev"),
)
NewExecutorTest(t,
WithName("explicit directory"),
WithExecutorOptions(task.WithDir(dir), task.WithEntrypoint(dir), task.WithSilent(true)),
WithTask("dev"),
)
}
37 changes: 37 additions & 0 deletions internal/flags/override_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package flags

import (
"bytes"
"os"
"path/filepath"
"runtime"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/go-task/task/v3"
)

func TestAutomaticOverrideGlobal(t *testing.T) { //nolint:paralleltest // changes process-wide flags and home directory
originalGlobal, originalDir, originalEntrypoint, originalTimeout := Global, Dir, Entrypoint, Timeout
t.Cleanup(func() {
Global, Dir, Entrypoint, Timeout = originalGlobal, originalDir, originalEntrypoint, originalTimeout
})
dir := t.TempDir()
homeKey := "HOME"
if runtime.GOOS == "windows" {
homeKey = "USERPROFILE"
}
t.Setenv(homeKey, dir)
require.NoError(t, os.WriteFile(filepath.Join(dir, "Taskfile.yml"), []byte("version: '3'\nvars: {PORT: 3000}\ntasks:\n dev: echo {{.PORT}}\n"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(dir, "Taskfile.override.yml"), []byte("version: '3'\nvars: {PORT: 4000}\n"), 0o600))

Global, Dir, Entrypoint, Timeout = true, "", "", time.Second
var stdout bytes.Buffer
e := task.NewExecutor(WithFlags(), task.WithStdout(&stdout), task.WithStderr(&stdout), task.WithSilent(true))
require.NoError(t, e.Setup())
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "dev"}))
assert.Equal(t, "4000\n", stdout.String())
}
16 changes: 14 additions & 2 deletions setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ import (

func (e *Executor) Setup() error {
e.setupLogger()
automaticOverride := discoversTaskfile(e.Entrypoint)
node, err := e.getRootNode()
if err != nil {
return err
}
if err := e.setupTempDir(); err != nil {
return err
}
if err := e.readTaskfile(node); err != nil {
if err := e.readTaskfile(node, automaticOverride); err != nil {
return err
}
e.setupStdFiles()
Expand All @@ -53,6 +54,16 @@ func (e *Executor) Setup() error {
return nil
}

// discoversTaskfile reports whether Task picks the Taskfile name itself, as
// opposed to the user naming the file.
func discoversTaskfile(entrypoint string) bool {
if entrypoint == "" {
return true
}
info, err := os.Stat(entrypoint)
return err == nil && info.IsDir()
}

func (e *Executor) getRootNode() (taskfile.Node, error) {
node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout,
taskfile.WithCACert(e.CACert),
Expand All @@ -72,7 +83,7 @@ func (e *Executor) getRootNode() (taskfile.Node, error) {
return node, err
}

func (e *Executor) readTaskfile(node taskfile.Node) error {
func (e *Executor) readTaskfile(node taskfile.Node, automaticOverride bool) error {
ctx, cf := context.WithTimeout(context.Background(), e.Timeout)
defer cf()
debugFunc := func(s string) {
Expand All @@ -82,6 +93,7 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
return e.Logger.Prompt(logger.Yellow, s, "n", "y", "yes")
}
reader := taskfile.NewReader(
taskfile.WithAutomaticOverride(automaticOverride),
taskfile.WithInsecure(e.Insecure),
taskfile.WithDownload(e.Download),
taskfile.WithOffline(e.Offline),
Expand Down
34 changes: 34 additions & 0 deletions taskfile/ast/override.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package ast

import (
"fmt"

"github.com/go-task/task/v3/errors"
)

// MergeOverride applies a local override: vars and env merge by key, tasks are
// replaced whole.
func (tf *Taskfile) MergeOverride(override *Taskfile) error {
if !tf.Version.Equal(override.Version) {
return &errors.TaskfileVersionCheckError{
URI: override.Location,
SchemaVersion: override.Version,
Message: fmt.Sprintf("must match the main Taskfile (%s)", tf.Version),
}
}
if tf.Vars == nil {
tf.Vars = NewVars()
}
if tf.Env == nil {
tf.Env = NewVars()
}
if tf.Tasks == nil {
tf.Tasks = NewTasks()
}
tf.Vars.Merge(override.Vars, nil)
tf.Env.Merge(override.Env, nil)
for name, task := range override.Tasks.All(nil) {
tf.Tasks.Set(name, task)
}
return nil
}
14 changes: 14 additions & 0 deletions taskfile/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ package taskfile

import "time"

// WithAutomaticOverride loads a Taskfile override beside a local root Taskfile.
// Remote, stdin and included Taskfiles never load one.
func WithAutomaticOverride(enabled bool) *automaticOverrideOption {
return &automaticOverrideOption{enabled: enabled}
}

type automaticOverrideOption struct {
enabled bool
}

func (o *automaticOverrideOption) ApplyToReader(r *Reader) {
r.automaticOverride = o.enabled
}

// WithInsecure allows the [Reader] to make insecure connections when reading
// remote taskfiles. By default, insecure connections are rejected.
func WithInsecure(insecure bool) *insecureOption {
Expand Down
61 changes: 61 additions & 0 deletions taskfile/override.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package taskfile

import (
"fmt"
"os"
"path/filepath"

"go.yaml.in/yaml/v3"

"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/taskfile/ast"
)

var defaultOverrides = []string{
"Taskfile.override.yml",
"taskfile.override.yml",
"Taskfile.override.yaml",
"taskfile.override.yaml",
}

func readAutomaticOverride(dir string, tf *ast.Taskfile) error {
for _, name := range defaultOverrides {
path := filepath.Join(dir, name)
// Check existence separately so a dangling symlink is an error too.
if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
continue
} else if err != nil {
return fmt.Errorf("task: Failed to read Taskfile override: %w", err)
}
b, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("task: Failed to read Taskfile override: %w", err)
}
override, err := decodeTaskfile(b, path, true)
if err != nil {
return err
}
return tf.MergeOverride(override)
}
return nil
}

// taskfileOverride restricts the root fields while using the normal Taskfile
// decoders for their values.
type taskfileOverride ast.Taskfile

func (tf *taskfileOverride) UnmarshalYAML(node *yaml.Node) error {
if node.Kind != yaml.MappingNode {
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("taskfile override")
}
for i := 0; i < len(node.Content); i += 2 {
key := node.Content[i]
switch key.Value {
case "version", "vars", "env", "tasks":
default:
return errors.NewTaskfileDecodeError(nil, key).
WithMessage("field %q is not allowed in a Taskfile override", key.Value)
}
}
return (*ast.Taskfile)(tf).UnmarshalYAML(node)
}
Loading
Loading