1
0
mirror of https://github.com/go-task/task.git synced 2024-12-04 10:24:45 +02:00

feat: root remote taskfiles

This commit is contained in:
Pete Davison 2024-02-13 01:07:00 +00:00
parent f00693052a
commit cbc19d35ea
12 changed files with 261 additions and 132 deletions

View File

@ -5,7 +5,6 @@ import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/spf13/pflag"
@ -94,11 +93,6 @@ func run() error {
dir = home
}
if entrypoint != "" {
dir = filepath.Dir(entrypoint)
entrypoint = filepath.Base(entrypoint)
}
var taskSorter sort.TaskSorter
switch flags.TaskSort {
case "none":

View File

@ -131,10 +131,6 @@ func Validate() error {
return nil
}
if Dir != "" && Entrypoint != "" {
return errors.New("task: You can't set both --dir and --taskfile")
}
if Output.Name != "group" {
if Output.Group.Begin != "" {
return errors.New("task: You can't set --output-group-begin without --output=group")

View File

@ -54,11 +54,11 @@ func (e *Executor) Setup() error {
}
func (e *Executor) getRootNode() (taskfile.Node, error) {
node, err := taskfile.NewRootNode(e.Dir, e.Entrypoint, e.Insecure)
node, err := taskfile.NewRootNode(e.Logger, e.Entrypoint, e.Dir, e.Insecure)
if err != nil {
return nil, err
}
e.Dir = node.BaseDir()
e.Dir = node.Dir()
return node, err
}

View File

@ -73,7 +73,7 @@ func (fct fileContentTest) Run(t *testing.T) {
for name, expectContent := range fct.Files {
t.Run(fct.name(name), func(t *testing.T) {
path := filepathext.SmartJoin(fct.Dir, name)
path := filepathext.SmartJoin(e.Dir, name)
b, err := os.ReadFile(path)
require.NoError(t, err, "Error reading file")
s := string(b)
@ -1123,8 +1123,8 @@ func TestIncludesOptionalExplicitFalse(t *testing.T) {
func TestIncludesFromCustomTaskfile(t *testing.T) {
tt := fileContentTest{
Entrypoint: "testdata/includes_yaml/Custom.ext",
Dir: "testdata/includes_yaml",
Entrypoint: "Custom.ext",
Target: "default",
TrimSpace: true,
Files: map[string]string{
@ -1486,16 +1486,12 @@ func TestDotenvShouldIncludeAllEnvFiles(t *testing.T) {
}
func TestDotenvShouldErrorWhenIncludingDependantDotenvs(t *testing.T) {
const dir = "testdata/dotenv/error_included_envs"
const entry = "Taskfile.yml"
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Entrypoint: entry,
Summary: true,
Stdout: &buff,
Stderr: &buff,
Dir: "testdata/dotenv/error_included_envs",
Summary: true,
Stdout: &buff,
Stderr: &buff,
}
err := e.Setup()

View File

@ -2,13 +2,9 @@ package ast
import (
"fmt"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
omap "github.com/go-task/task/v3/internal/omap"
)
@ -22,7 +18,6 @@ type Include struct {
Aliases []string
AdvancedImport bool
Vars *Vars
BaseDir string // The directory from which the including taskfile was loaded; used to resolve relative paths
}
// Includes represents information about included tasksfiles
@ -120,39 +115,5 @@ func (include *Include) DeepCopy() *Include {
Internal: include.Internal,
AdvancedImport: include.AdvancedImport,
Vars: include.Vars.DeepCopy(),
BaseDir: include.BaseDir,
}
}
// FullTaskfilePath returns the fully qualified path to the included taskfile
func (include *Include) FullTaskfilePath() (string, error) {
return include.resolvePath(include.Taskfile)
}
// FullDirPath returns the fully qualified path to the included taskfile's working directory
func (include *Include) FullDirPath() (string, error) {
return include.resolvePath(include.Dir)
}
func (include *Include) resolvePath(path string) (string, error) {
// If the file is remote, we don't need to resolve the path
if strings.Contains(include.Taskfile, "://") {
return path, nil
}
path, err := execext.Expand(path)
if err != nil {
return "", err
}
if filepathext.IsAbs(path) {
return path, nil
}
result, err := filepath.Abs(filepathext.SmartJoin(include.BaseDir, path))
if err != nil {
return "", fmt.Errorf("task: error resolving path %s relative to %s: %w", path, include.BaseDir, err)
}
return result, nil
}

View File

@ -8,20 +8,25 @@ import (
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/taskfile/ast"
)
type Node interface {
Read(ctx context.Context) ([]byte, error)
Parent() Node
Location() string
Dir() string
Optional() bool
Remote() bool
BaseDir() string
ResolveIncludeEntrypoint(include ast.Include) (string, error)
ResolveIncludeDir(include ast.Include) (string, error)
}
func NewRootNode(
dir string,
l *logger.Logger,
entrypoint string,
dir string,
insecure bool,
) (Node, error) {
dir = getDefaultDir(entrypoint, dir)
@ -30,32 +35,24 @@ func NewRootNode(
if (stat.Mode()&os.ModeCharDevice) == 0 && stat.Size() > 0 {
return NewStdinNode(dir)
}
// If no entrypoint is specified, search for a taskfile
if entrypoint == "" {
root, err := ExistsWalk(dir)
if err != nil {
return nil, err
}
return NewNode(root, insecure)
}
// Use the specified entrypoint
uri := filepath.Join(dir, entrypoint)
return NewNode(uri, insecure)
return NewNode(l, entrypoint, dir, insecure)
}
func NewNode(
uri string,
l *logger.Logger,
entrypoint string,
dir string,
insecure bool,
opts ...NodeOption,
) (Node, error) {
var node Node
var err error
switch getScheme(uri) {
switch getScheme(entrypoint) {
case "http", "https":
node, err = NewHTTPNode(uri, insecure, opts...)
node, err = NewHTTPNode(l, entrypoint, dir, insecure, opts...)
default:
// If no other scheme matches, we assume it's a file
node, err = NewFileNode(uri, opts...)
node, err = NewFileNode(l, entrypoint, dir, opts...)
}
if node.Remote() && !experiments.RemoteTaskfiles.Enabled {
return nil, errors.New("task: Remote taskfiles are not enabled. You can read more about this experiment and how to enable it at https://taskfile.dev/experiments/remote-taskfiles")

View File

@ -9,6 +9,7 @@ type (
BaseNode struct {
parent Node
optional bool
dir string
}
)
@ -16,6 +17,7 @@ func NewBaseNode(opts ...NodeOption) *BaseNode {
node := &BaseNode{
parent: nil,
optional: false,
dir: "",
}
// Apply options
@ -45,3 +47,7 @@ func WithOptional(optional bool) NodeOption {
func (node *BaseNode) Optional() bool {
return node.optional
}
func (node *BaseNode) Dir() string {
return node.dir
}

View File

@ -5,39 +5,36 @@ import (
"io"
"os"
"path/filepath"
"strings"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/taskfile/ast"
)
// A FileNode is a node that reads a taskfile from the local filesystem.
type FileNode struct {
*BaseNode
Dir string
Entrypoint string
}
func NewFileNode(uri string, opts ...NodeOption) (*FileNode, error) {
func NewFileNode(l *logger.Logger, entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
var err error
base := NewBaseNode(opts...)
if uri == "" {
d, err := os.Getwd()
if err != nil {
return nil, err
}
uri = d
}
path, err := Exists(uri)
entrypoint, dir, err = resolveFileNodeEntrypointAndDir(l, entrypoint, dir)
if err != nil {
return nil, err
}
base.dir = dir
return &FileNode{
BaseNode: base,
Dir: filepath.Dir(path),
Entrypoint: filepath.Base(path),
Entrypoint: entrypoint,
}, nil
}
func (node *FileNode) Location() string {
return filepathext.SmartJoin(node.Dir, node.Entrypoint)
return node.Entrypoint
}
func (node *FileNode) Remote() bool {
@ -53,6 +50,67 @@ func (node *FileNode) Read(ctx context.Context) ([]byte, error) {
return io.ReadAll(f)
}
func (node *FileNode) BaseDir() string {
return node.Dir
// resolveFileNodeEntrypointAndDir resolves checks the values of entrypoint and dir and
// populates them with default values if necessary.
func resolveFileNodeEntrypointAndDir(l *logger.Logger, entrypoint, dir string) (string, string, error) {
var err error
if entrypoint != "" {
entrypoint, err = Exists(l, entrypoint)
if err != nil {
return "", "", err
}
if dir == "" {
dir = filepath.Dir(entrypoint)
}
return entrypoint, dir, nil
}
if dir == "" {
dir, err = os.Getwd()
if err != nil {
return "", "", err
}
}
entrypoint, err = ExistsWalk(l, dir)
if err != nil {
return "", "", err
}
dir = filepath.Dir(entrypoint)
return entrypoint, dir, nil
}
func (node *FileNode) ResolveIncludeEntrypoint(include ast.Include) (string, error) {
// If the file is remote, we don't need to resolve the path
if strings.Contains(include.Taskfile, "://") {
return include.Taskfile, nil
}
path, err := execext.Expand(include.Taskfile)
if err != nil {
return "", err
}
if filepathext.IsAbs(path) {
return path, nil
}
// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory
// This means that files are included relative to one another
entrypointDir := filepath.Dir(node.Entrypoint)
return filepathext.SmartJoin(entrypointDir, path), nil
}
func (node *FileNode) ResolveIncludeDir(include ast.Include) (string, error) {
path, err := execext.Expand(include.Dir)
if err != nil {
return "", err
}
if filepathext.IsAbs(path) {
return path, nil
}
// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory
// This means that files are included relative to one another
entrypointDir := filepath.Dir(node.Entrypoint)
return filepathext.SmartJoin(entrypointDir, path), nil
}

View File

@ -5,8 +5,13 @@ import (
"io"
"net/http"
"net/url"
"path/filepath"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/taskfile/ast"
)
// An HTTPNode is a node that reads a Taskfile from a remote location via HTTP.
@ -15,14 +20,19 @@ type HTTPNode struct {
URL *url.URL
}
func NewHTTPNode(uri string, insecure bool, opts ...NodeOption) (*HTTPNode, error) {
func NewHTTPNode(l *logger.Logger, entrypoint, dir string, insecure bool, opts ...NodeOption) (*HTTPNode, error) {
base := NewBaseNode(opts...)
url, err := url.Parse(uri)
base.dir = dir
url, err := url.Parse(entrypoint)
if err != nil {
return nil, err
}
if url.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: uri}
return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
}
url, err = RemoteExists(l, url)
if err != nil {
return nil, err
}
return &HTTPNode{
BaseNode: base,
@ -66,6 +76,26 @@ func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) {
return b, nil
}
func (node *HTTPNode) BaseDir() string {
return ""
func (node *HTTPNode) ResolveIncludeEntrypoint(include ast.Include) (string, error) {
ref, err := url.Parse(include.Taskfile)
if err != nil {
return "", err
}
return node.URL.ResolveReference(ref).String(), nil
}
func (node *HTTPNode) ResolveIncludeDir(include ast.Include) (string, error) {
path, err := execext.Expand(include.Dir)
if err != nil {
return "", err
}
if filepathext.IsAbs(path) {
return path, nil
}
// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory
// This means that files are included relative to one another
entrypointDir := filepath.Dir(node.Dir())
return filepathext.SmartJoin(entrypointDir, path), nil
}

View File

@ -5,19 +5,23 @@ import (
"context"
"fmt"
"os"
"strings"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/taskfile/ast"
)
// A StdinNode is a node that reads a taskfile from the standard input stream.
type StdinNode struct {
*BaseNode
Dir string
}
func NewStdinNode(dir string) (*StdinNode, error) {
base := NewBaseNode()
base.dir = dir
return &StdinNode{
BaseNode: base,
Dir: dir,
}, nil
}
@ -41,6 +45,33 @@ func (node *StdinNode) Read(ctx context.Context) ([]byte, error) {
return stdin, nil
}
func (node *StdinNode) BaseDir() string {
return node.Dir
func (node *StdinNode) ResolveIncludeEntrypoint(include ast.Include) (string, error) {
// If the file is remote, we don't need to resolve the path
if strings.Contains(include.Taskfile, "://") {
return include.Taskfile, nil
}
path, err := execext.Expand(include.Taskfile)
if err != nil {
return "", err
}
if filepathext.IsAbs(path) {
return path, nil
}
return filepathext.SmartJoin(node.Dir(), path), nil
}
func (node *StdinNode) ResolveIncludeDir(include ast.Include) (string, error) {
path, err := execext.Expand(include.Dir)
if err != nil {
return "", err
}
if filepathext.IsAbs(path) {
return path, nil
}
return filepathext.SmartJoin(node.Dir(), path), nil
}

View File

@ -48,17 +48,6 @@ func Read(
return nil, &errors.TaskfileVersionCheckError{URI: node.Location()}
}
if dir := node.BaseDir(); dir != "" {
_ = tf.Includes.Range(func(namespace string, include ast.Include) error {
// Set the base directory for resolving relative paths, but only if not already set
if include.BaseDir == "" {
include.BaseDir = dir
tf.Includes.Set(namespace, include)
}
return nil
})
}
err = tf.Includes.Range(func(namespace string, include ast.Include) error {
cache := &templater.Cache{Vars: tf.Vars}
include = ast.Include{
@ -70,18 +59,22 @@ func Read(
Aliases: include.Aliases,
AdvancedImport: include.AdvancedImport,
Vars: include.Vars,
BaseDir: include.BaseDir,
}
if err := cache.Err(); err != nil {
return err
}
uri, err := include.FullTaskfilePath()
entrypoint, err := node.ResolveIncludeEntrypoint(include)
if err != nil {
return err
}
includeReaderNode, err := NewNode(uri, insecure,
dir, err := node.ResolveIncludeDir(include)
if err != nil {
return err
}
includeReaderNode, err := NewNode(l, entrypoint, dir, insecure,
WithParent(node),
WithOptional(include.Optional),
)
@ -109,11 +102,6 @@ func Read(
}
if include.AdvancedImport {
dir, err := include.FullDirPath()
if err != nil {
return err
}
// nolint: errcheck
includedTaskfile.Vars.Range(func(k string, v ast.Var) error {
o := v

View File

@ -1,11 +1,16 @@
package taskfile
import (
"net/http"
"net/url"
"os"
"path/filepath"
"slices"
"strings"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/sysinfo"
)
@ -23,14 +28,80 @@ var (
"Taskfile.dist.yaml",
"taskfile.dist.yaml",
}
allowedContentTypes = []string{
"text/plain",
"text/yaml",
"text/x-yaml",
"application/yaml",
"application/x-yaml",
}
)
// RemoteExists will check if a file at the given URL Exists. If it does, it
// will return its URL. If it does not, it will search the search for any files
// at the given URL with any of the default Taskfile files names. If any of
// these match a file, the first matching path will be returned. If no files are
// found, an error will be returned.
func RemoteExists(l *logger.Logger, u *url.URL) (*url.URL, error) {
// Create a new HEAD request for the given URL to check if the resource exists
req, err := http.NewRequest("HEAD", u.String(), nil)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: u.String()}
}
// Request the given URL
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: u.String()}
}
defer resp.Body.Close()
// If the request was successful and the content type is allowed, return the
// URL The content type check is to avoid downloading files that are not
// Taskfiles It means we can try other files instead of downloading
// something that is definitely not a Taskfile
contentType := resp.Header.Get("Content-Type")
if resp.StatusCode == http.StatusOK && slices.ContainsFunc(allowedContentTypes, func(s string) bool {
return strings.Contains(contentType, s)
}) {
return u, nil
}
// If the request was not successful, append the default Taskfile names to
// the URL and return the URL of the first successful request
for _, taskfile := range defaultTaskfiles {
// Fixes a bug with JoinPath where a leading slash is not added to the
// path if it is empty
if u.Path == "" {
u.Path = "/"
}
alt := u.JoinPath(taskfile)
req.URL = alt
// Try the alternative URL
resp, err = http.DefaultClient.Do(req)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: u.String()}
}
defer resp.Body.Close()
// If the request was successful, return the URL
if resp.StatusCode == http.StatusOK {
l.VerboseOutf(logger.Magenta, "task: [%s] Not found - Using alternative (%s)\n", alt.String(), taskfile)
return alt, nil
}
}
return nil, errors.TaskfileNotFoundError{URI: u.String(), Walk: false}
}
// Exists will check if a file at the given path Exists. If it does, it will
// return the path to it. If it does not, it will search the search for any
// files at the given path with any of the default Taskfile files names. If any
// of these match a file, the first matching path will be returned. If no files
// are found, an error will be returned.
func Exists(path string) (string, error) {
// return the path to it. If it does not, it will search for any files at the
// given path with any of the default Taskfile files names. If any of these
// match a file, the first matching path will be returned. If no files are
// found, an error will be returned.
func Exists(l *logger.Logger, path string) (string, error) {
fi, err := os.Stat(path)
if err != nil {
return "", err
@ -42,10 +113,11 @@ func Exists(path string) (string, error) {
return filepath.Abs(path)
}
for _, n := range defaultTaskfiles {
fpath := filepathext.SmartJoin(path, n)
if _, err := os.Stat(fpath); err == nil {
return filepath.Abs(fpath)
for _, taskfile := range defaultTaskfiles {
alt := filepathext.SmartJoin(path, taskfile)
if _, err := os.Stat(alt); err == nil {
l.VerboseOutf(logger.Magenta, "task: [%s] Not found - Using alternative (%s)\n", path, taskfile)
return filepath.Abs(alt)
}
}
@ -57,14 +129,14 @@ func Exists(path string) (string, error) {
// calling the exists function until it finds a file or reaches the root
// directory. On supported operating systems, it will also check if the user ID
// of the directory changes and abort if it does.
func ExistsWalk(path string) (string, error) {
func ExistsWalk(l *logger.Logger, path string) (string, error) {
origPath := path
owner, err := sysinfo.Owner(path)
if err != nil {
return "", err
}
for {
fpath, err := Exists(path)
fpath, err := Exists(l, path)
if err == nil {
return fpath, nil
}