diff --git a/cmd/run/main.go b/cmd/run/main.go index 9787a59..1437659 100644 --- a/cmd/run/main.go +++ b/cmd/run/main.go @@ -9,23 +9,21 @@ import ( "strings" "syscall" - "github.com/nxtcoder17/fwatcher/pkg/logging" + "github.com/nxtcoder17/runfile/pkg/logging" "github.com/nxtcoder17/runfile/pkg/runfile" + "github.com/nxtcoder17/runfile/pkg/runfile/errors" "github.com/urfave/cli/v3" ) -var ( - Version string = "0.0.1" - runfileNames []string = []string{ - "Runfile", - "Runfile.yml", - "Runfile.yaml", - } -) +var Version string = "nightly" -func main() { - logger := logging.NewSlogLogger(logging.SlogOptions{}) +var runfileNames []string = []string{ + "Runfile", + "Runfile.yml", + "Runfile.yaml", +} +func main() { cmd := cli.Command{ Name: "run", Version: Version, @@ -166,7 +164,10 @@ func main() { }() if err := cmd.Run(ctx, os.Args); err != nil { - logger.Error(err.Error()) + errm, ok := err.(errors.Message) + if ok { + errm.Log() + } os.Exit(1) } } diff --git a/pkg/logging/logger.go b/pkg/logging/logger.go new file mode 100644 index 0000000..8b2453d --- /dev/null +++ b/pkg/logging/logger.go @@ -0,0 +1,81 @@ +package logging + +import ( + "io" + "log/slog" + "os" + + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/log" +) + +type SlogOptions struct { + Writer io.Writer + Prefix string + + ShowTimestamp bool + ShowCaller bool + ShowDebugLogs bool + + SetAsDefaultLogger bool +} + +func NewSlogLogger(opts SlogOptions) *slog.Logger { + // INFO: force colored output, otherwise honor the env-var `CLICOLOR_FORCE` + if _, ok := os.LookupEnv("CLICOLOR_FORCE"); !ok { + os.Setenv("CLICOLOR_FORCE", "1") + } + + if opts.Writer == nil { + opts.Writer = os.Stderr + } + + level := log.InfoLevel + if opts.ShowDebugLogs { + level = log.DebugLevel + } + + logger := log.NewWithOptions(opts.Writer, log.Options{ + ReportCaller: opts.ShowCaller, + ReportTimestamp: opts.ShowTimestamp, + Prefix: opts.Prefix, + Level: level, + }) + + styles := log.DefaultStyles() + // styles.Caller = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Dark: "#5b717f", Light: "#36cbfa"}).Faint(true) + styles.Caller = lipgloss.NewStyle().Foreground(lipgloss.Color("#878a8a")) + + styles.Levels[log.ErrorLevel] = lipgloss.NewStyle(). + SetString("ERROR"). + Padding(0, 1, 0, 1). + // Background(lipgloss.Color("204")). + Foreground(lipgloss.Color("202")) + + styles.Levels[log.DebugLevel] = styles.Levels[log.DebugLevel].Foreground(lipgloss.Color("#5b717f")) + + styles.Levels[log.InfoLevel] = styles.Levels[log.InfoLevel].Foreground(lipgloss.Color("#36cbfa")) + + // BUG: due to a bug in termenv, adaptive colors don't work within tmux + // it always selects the dark variant + + // styles.Levels[log.InfoLevel] = styles.Levels[log.InfoLevel].Foreground(lipgloss.AdaptiveColor{ + // Light: string(lipgloss.Color("#36cbfa")), + // Dark: string(lipgloss.Color("#608798")), + // }) + + styles.Key = lipgloss.NewStyle().Foreground(lipgloss.Color("#36cbfa")).Bold(true) + + logger.SetStyles(styles) + + // output := termenv.NewOutput(os.Stdout, termenv.WithProfile(termenv.TrueColor)) + // logger.Info("theme", "fg", output.ForegroundColor(), "bg", output.BackgroundColor(), "has-dark", output.HasDarkBackground()) + + l := slog.New(logger) + + if opts.SetAsDefaultLogger { + slog.SetDefault(l) + } + + return l +} diff --git a/pkg/runfile/types.go b/pkg/runfile/context.go similarity index 73% rename from pkg/runfile/types.go rename to pkg/runfile/context.go index ae81759..44c812b 100644 --- a/pkg/runfile/types.go +++ b/pkg/runfile/context.go @@ -12,11 +12,15 @@ import ( sprig "github.com/go-task/slim-sprig/v3" fn "github.com/nxtcoder17/runfile/pkg/functions" + "github.com/nxtcoder17/runfile/pkg/runfile/errors" ) type Context struct { context.Context *slog.Logger + + RunfilePath string + Taskname string } func NewContext(ctx context.Context, logger *slog.Logger) Context { @@ -49,7 +53,7 @@ type EnvKV struct { GoTmpl *string `json:"gotmpl"` } -func (ejv EnvKV) Parse(ctx Context, args EvaluationArgs) (*string, error) { +func (ejv EnvKV) Parse(ctx Context, args EvaluationArgs) (*string, errors.Message) { switch { case ejv.Value != nil: { @@ -66,7 +70,7 @@ func (ejv EnvKV) Parse(ctx Context, args EvaluationArgs) (*string, error) { stdout: value, }) if err := cmd.Run(); err != nil { - return nil, err + return nil, errors.TaskEnvCommandFailed.WithErr(err) } return fn.New(strings.TrimSpace(value.String())), nil @@ -76,33 +80,34 @@ func (ejv EnvKV) Parse(ctx Context, args EvaluationArgs) (*string, error) { t := template.New(ejv.Key).Funcs(sprig.FuncMap()) t, err := t.Parse(fmt.Sprintf(`{{ %s }}`, *ejv.GoTmpl)) if err != nil { - return nil, err + return nil, errors.TaskEnvGoTmplFailed.WithErr(err) } value := new(bytes.Buffer) if err := t.ExecuteTemplate(value, ejv.Key, map[string]string{}); err != nil { - return nil, err + return nil, errors.TaskEnvGoTmplFailed.WithErr(err) } return fn.New(strings.TrimSpace(value.String())), nil } default: { - return nil, fmt.Errorf("failed to parse, unknown format, one of [value, sh, gotmpl] must be set") + return nil, errors.TaskEnvInvalid.WithErr(fmt.Errorf("failed to parse, unknown format, one of [value, sh, gotmpl] must be set")) } } } -func parseEnvVars(ctx Context, ev EnvVar, args EvaluationArgs) (map[string]string, error) { +func parseEnvVars(ctx Context, ev EnvVar, args EvaluationArgs) (map[string]string, errors.Message) { env := make(map[string]string, len(ev)) for k, v := range ev { + attr := []any{slog.Group("env", "key", k, "value", v)} switch v := v.(type) { case string: env[k] = v case map[string]any: b, err := json.Marshal(v) if err != nil { - return nil, err + return nil, errors.TaskEnvInvalid.WithErr(err).WithMetadata(attr) } var envAsJson struct { @@ -112,7 +117,7 @@ func parseEnvVars(ctx Context, ev EnvVar, args EvaluationArgs) (map[string]strin } if err := json.Unmarshal(b, &envAsJson); err != nil { - return nil, err + return nil, errors.TaskEnvInvalid.WithErr(err).WithMetadata(attr) } switch { @@ -130,7 +135,7 @@ func parseEnvVars(ctx Context, ev EnvVar, args EvaluationArgs) (map[string]strin } if !isDefined { - return nil, fmt.Errorf("env: %q, not defined", k) + return nil, errors.TaskEnvInvalid.WithErr(fmt.Errorf("env required, but not provided")).WithMetadata(attr) } } @@ -139,7 +144,7 @@ func parseEnvVars(ctx Context, ev EnvVar, args EvaluationArgs) (map[string]strin envAsJson.Key = k s, err := envAsJson.EnvKV.Parse(ctx, args) if err != nil { - return nil, err + return nil, err.WithMetadata(attr) } env[k] = *s } @@ -149,13 +154,13 @@ func parseEnvVars(ctx Context, ev EnvVar, args EvaluationArgs) (map[string]strin envAsJson.Default.Key = k s, err := envAsJson.Default.Parse(ctx, args) if err != nil { - return nil, err + return nil, err.WithMetadata(attr) } env[k] = *s } default: { - return nil, fmt.Errorf("either required, value, sh, gotmpl or default, must be defined") + return nil, errors.TaskEnvInvalid.WithErr(fmt.Errorf("either required, value, sh, gotmpl or default, must be defined")).WithMetadata(attr) } } diff --git a/pkg/runfile/errors/errors.go b/pkg/runfile/errors/errors.go deleted file mode 100644 index a3b8049..0000000 --- a/pkg/runfile/errors/errors.go +++ /dev/null @@ -1,135 +0,0 @@ -package errors - -import ( - "encoding/json" - "fmt" -) - -type Context struct { - Verbose bool - - Task string - Runfile string - - message *string - err error -} - -func (c Context) WithMessage(msg string) Context { - c.message = &msg - return c -} - -func (c Context) WithErr(err error) Context { - c.err = err - return c -} - -func (c Context) ToString() string { - if !c.Verbose { - if c.message != nil { - return fmt.Sprintf("[%s] %s, got err: %v", c.Task, *c.message, c.err) - } - return fmt.Sprintf("[%s] got err: %v", c.Task, c.err) - } - - m := map[string]string{ - "task": c.Task, - "runfile": c.Runfile, - } - if c.message != nil { - m["message"] = *c.message - } - - if c.err != nil { - m["err"] = c.err.Error() - } - - b, err := json.Marshal(m) - if err != nil { - panic(err) - } - - return string(b) -} - -func (e Context) Error() string { - return e.ToString() -} - -type ( - ErrTaskInvalid struct{ Context } -) - -type ErrTaskFailedRequirements struct { - Context - Requirement string -} - -func (e ErrTaskFailedRequirements) Error() string { - if e.message == nil { - e.Context = e.Context.WithMessage(fmt.Sprintf("failed (requirement: %q)", e.Requirement)) - } - return e.Context.Error() -} - -type TaskNotFound struct { - Context -} - -func (e TaskNotFound) Error() string { - // return fmt.Sprintf("[task] %s, not found in [Runfile] at %s", e.TaskName, e.RunfilePath) - if e.message == nil { - e.Context = e.Context.WithMessage("Not Found") - } - return e.Context.Error() -} - -type ErrTaskGeneric struct { - Context -} - -type InvalidWorkingDirectory struct { - Context -} - -func (e InvalidWorkingDirectory) Error() string { - // return fmt.Sprintf("[task] %s, not found in [Runfile] at %s", e.TaskName, e.RunfilePath) - if e.message == nil { - e.Context = e.Context.WithMessage("Invalid Working Directory") - } - return e.Context.Error() -} - -type InvalidDotEnv struct { - Context -} - -func (e InvalidDotEnv) Error() string { - if e.message == nil { - e.Context = e.Context.WithMessage("invalid dotenv") - } - return e.Context.Error() -} - -type InvalidEnvVar struct { - Context -} - -func (e InvalidEnvVar) Error() string { - if e.message == nil { - e.Context = e.Context.WithMessage("invalid dotenv") - } - return e.Context.Error() -} - -type IncorrectCommand struct { - Context -} - -func (e IncorrectCommand) Error() string { - if e.message == nil { - e.Context = e.Context.WithMessage("incorrect command") - } - return e.Context.Error() -} diff --git a/pkg/runfile/errors/known-errors.go b/pkg/runfile/errors/known-errors.go new file mode 100644 index 0000000..5b4e3db --- /dev/null +++ b/pkg/runfile/errors/known-errors.go @@ -0,0 +1,33 @@ +package errors + +// runfile +var ( + RunfileReadFailed = New("Runfile Read Failed", nil) + RunfileParsingFailed = New("Runfile Parsing Failed", nil) +) + +var ( + TaskNotFound = New("Task Not Found", nil) + TaskFailed = New("Task Failed", nil) + + TaskWorkingDirectoryInvalid = New("Task Working Directory Invalid", nil) + + TaskRequirementFailed = New("Task Requirement Failed", nil) + TaskRequirementIncorrect = New("Task Requirement Incorrect", nil) + + TaskEnvInvalid = New("Task Env is invalid", nil) + TaskEnvRequired = New("Task Env is Required", nil) + TaskEnvCommandFailed = New("Task Env command failed", nil) + TaskEnvGoTmplFailed = New("Task Env GoTemplate failed", nil) +) + +var ( + DotEnvNotFound = New("DotEnv Not Found", nil) + DotEnvInvalid = New("Dotenv Invalid", nil) + DotEnvParsingFailed = New("DotEnv Parsing Failed", nil) +) + +var ( + CommandFailed = New("Command Failed", nil) + CommandInvalid = New("Command Invalid", nil) +) diff --git a/pkg/runfile/errors/message.go b/pkg/runfile/errors/message.go new file mode 100644 index 0000000..e5c6604 --- /dev/null +++ b/pkg/runfile/errors/message.go @@ -0,0 +1,67 @@ +package errors + +import ( + "encoding/json" + "log/slog" +) + +type Message interface { + error + + WithMetadata(attrs ...any) *message + Log() +} + +type message struct { + text string + err error + + metadata []any +} + +func New(text string, err error) *message { + return &message{ + text: text, + err: err, + } +} + +func (m *message) WithErr(err error) *message { + m.err = err + return m +} + +func (m *message) WithMetadata(metaAttrs ...any) *message { + maxlen := len(metaAttrs) + if len(metaAttrs)&1 == 1 { + // INFO: if odd, leave last item + maxlen -= 1 + } + + for i := 0; i < maxlen; i += 2 { + m.metadata = append(m.metadata, metaAttrs[i], metaAttrs[i+1]) + } + + return m +} + +func (m *message) Error() string { + b, err := json.Marshal(map[string]any{ + "text": m.text, + "error": m.err.Error(), + "metadata": m.metadata, + }) + if err != nil { + panic(err) + } + + return string(b) +} + +func (m *message) Log() { + if m.err == nil { + slog.Error(m.text) + return + } + slog.Error(m.text, "err", m.err) +} diff --git a/pkg/runfile/parser.go b/pkg/runfile/parser.go index 91c2235..fb776c3 100644 --- a/pkg/runfile/parser.go +++ b/pkg/runfile/parser.go @@ -7,28 +7,35 @@ import ( "path/filepath" "github.com/joho/godotenv" + "github.com/nxtcoder17/runfile/pkg/runfile/errors" ) -func parseDotEnv(reader io.Reader) (map[string]string, error) { - return godotenv.Parse(reader) +func parseDotEnv(reader io.Reader) (map[string]string, errors.Message) { + m, err := godotenv.Parse(reader) + if err != nil { + return nil, errors.DotEnvParsingFailed.WithErr(err) + } + return m, nil } // parseDotEnv parses the .env file and returns a slice of strings as in os.Environ() -func parseDotEnvFiles(files ...string) (map[string]string, error) { + +func parseDotEnvFiles(files ...string) (map[string]string, errors.Message) { results := make(map[string]string) for i := range files { if !filepath.IsAbs(files[i]) { - return nil, fmt.Errorf("dotenv file path %s, must be absolute", files[i]) + return nil, errors.DotEnvInvalid.WithErr(fmt.Errorf("dotenv file paths must be absolute")).WithMetadata("dotenv", files[i]) } f, err := os.Open(files[i]) if err != nil { - return nil, err + return nil, errors.DotEnvInvalid.WithErr(err).WithMetadata("dotenv", files[i]) } - m, err := parseDotEnv(f) - if err != nil { - return nil, err + + m, err2 := parseDotEnv(f) + if err2 != nil { + return nil, err2.WithMetadata("dotenv", files[i]) } f.Close() @@ -40,3 +47,25 @@ func parseDotEnvFiles(files ...string) (map[string]string, error) { return results, nil } + +func ParseIncludes(rf *Runfile) (map[string]ParsedIncludeSpec, errors.Message) { + m := make(map[string]ParsedIncludeSpec, len(rf.Includes)) + for k, v := range rf.Includes { + r, err := Parse(v.Runfile) + if err != nil { + return nil, err + } + + for it := range r.Tasks { + if v.Dir != "" { + nt := r.Tasks[it] + nt.Dir = &v.Dir + r.Tasks[it] = nt + } + } + + m[k] = ParsedIncludeSpec{Runfile: r} + } + + return m, nil +} diff --git a/pkg/runfile/run.go b/pkg/runfile/run.go index f8643ca..215a190 100644 --- a/pkg/runfile/run.go +++ b/pkg/runfile/run.go @@ -3,7 +3,6 @@ package runfile import ( "fmt" "io" - "log/slog" "os" "os/exec" @@ -43,6 +42,7 @@ func createCommand(ctx Context, args cmdArgs) *exec.Cmd { c.Env = append(os.Environ(), args.env...) c.Stdout = args.stdout c.Stderr = args.stderr + return c } @@ -51,12 +51,14 @@ type runTaskArgs struct { envOverrides map[string]string } -func (rf *Runfile) runTask(ctx Context, args runTaskArgs) error { - logger := ctx.Logger.With("runfile", rf.attrs.RunfilePath, "task", args.taskName, "env:overrides", args.envOverrides) +func (rf *Runfile) runTask(ctx Context, args runTaskArgs) errors.Message { + attr := []any{"task", args.taskName, "runfile", rf.attrs.RunfilePath} + + logger := ctx.With("runfile", rf.attrs.RunfilePath, "task", args.taskName, "env:overrides", args.envOverrides) logger.Debug("running task") task, ok := rf.Tasks[args.taskName] if !ok { - return errors.TaskNotFound{Context: errors.Context{Runfile: rf.attrs.RunfilePath, Task: args.taskName}} + return errors.TaskNotFound.WithMetadata(attr) } task.Name = args.taskName @@ -66,14 +68,13 @@ func (rf *Runfile) runTask(ctx Context, args runTaskArgs) error { for k, v := range args.envOverrides { task.Env[k] = v } - pt, err := ParseTask(ctx, rf, &task) + pt, err := ParseTask(ctx, rf, task) if err != nil { return err } // envVars := append(pt.Environ, args.envOverrides...) ctx.Debug("debugging env", "pt.environ", pt.Env, "overrides", args.envOverrides, "task", args.taskName) - for _, command := range pt.Commands { if command.Run != "" { if err := rf.runTask(ctx, runTaskArgs{ @@ -93,7 +94,7 @@ func (rf *Runfile) runTask(ctx Context, args runTaskArgs) error { workingDir: pt.WorkingDir, }) if err := cmd.Run(); err != nil { - return err + return errors.CommandFailed.WithErr(err).WithMetadata(attr) } } @@ -108,8 +109,7 @@ type RunArgs struct { KVs map[string]string } -func (rf *Runfile) Run(ctx Context, args RunArgs) error { - ctx.Debug("run", "tasks", args.Tasks) +func (rf *Runfile) Run(ctx Context, args RunArgs) errors.Message { includes, err := rf.ParseIncludes() if err != nil { return err @@ -126,10 +126,7 @@ func (rf *Runfile) Run(ctx Context, args RunArgs) error { task, ok := rf.Tasks[taskName] if !ok { - return errors.TaskNotFound{Context: errors.Context{ - Task: taskName, - Runfile: rf.attrs.RunfilePath, - }} + return errors.TaskNotFound.WithMetadata("task", taskName, "runfile", rf.attrs.RunfilePath) } // INFO: adding parsed KVs as environments to the specified tasks @@ -144,7 +141,7 @@ func (rf *Runfile) Run(ctx Context, args RunArgs) error { } if args.ExecuteInParallel { - slog.Default().Debug("running in parallel mode", "tasks", args.Tasks) + ctx.Debug("running in parallel mode", "tasks", args.Tasks) g := new(errgroup.Group) for _, _tn := range args.Tasks { @@ -156,7 +153,7 @@ func (rf *Runfile) Run(ctx Context, args RunArgs) error { // Wait for all tasks to finish if err := g.Wait(); err != nil { - return err + return errors.TaskFailed.WithErr(err).WithMetadata("task", args.Tasks, "runfile", rf.attrs.RunfilePath) } return nil diff --git a/pkg/runfile/runfile.go b/pkg/runfile/runfile.go index 7e0facb..e6d5495 100644 --- a/pkg/runfile/runfile.go +++ b/pkg/runfile/runfile.go @@ -5,6 +5,7 @@ import ( "path/filepath" fn "github.com/nxtcoder17/runfile/pkg/functions" + "github.com/nxtcoder17/runfile/pkg/runfile/errors" "sigs.k8s.io/yaml" ) @@ -29,26 +30,26 @@ type ParsedIncludeSpec struct { Runfile *Runfile } -func Parse(file string) (*Runfile, error) { +func Parse(file string) (*Runfile, errors.Message) { var runfile Runfile f, err := os.ReadFile(file) if err != nil { - return &runfile, err + return &runfile, errors.RunfileReadFailed.WithErr(err).WithMetadata("file", file) } if err = yaml.Unmarshal(f, &runfile); err != nil { - return nil, err + return nil, errors.RunfileParsingFailed.WithErr(err).WithMetadata("file", file) } runfile.attrs.RunfilePath = fn.Must(filepath.Abs(file)) return &runfile, nil } -func (rf *Runfile) ParseIncludes() (map[string]ParsedIncludeSpec, error) { +func (rf *Runfile) ParseIncludes() (map[string]ParsedIncludeSpec, errors.Message) { m := make(map[string]ParsedIncludeSpec, len(rf.Includes)) for k, v := range rf.Includes { r, err := Parse(v.Runfile) if err != nil { - return nil, err + return nil, err.WithMetadata("includes", v.Runfile) } for it := range r.Tasks { diff --git a/pkg/runfile/task-parser.go b/pkg/runfile/task-parser.go index 4aeb826..47d2739 100644 --- a/pkg/runfile/task-parser.go +++ b/pkg/runfile/task-parser.go @@ -21,13 +21,8 @@ type ParsedTask struct { } // func ParseTask(ctx Context, rf *Runfile, taskName string) (*ParsedTask, error) { -func ParseTask(ctx Context, rf *Runfile, task *Task) (*ParsedTask, error) { - if task == nil { - return nil, fmt.Errorf("task does not exist") - } - - errctx := errors.Context{Task: task.Name, Runfile: rf.attrs.RunfilePath} - +func ParseTask(ctx Context, rf *Runfile, task Task) (*ParsedTask, errors.Message) { + attrs := []any{"task", task.Name, "runfile", rf.attrs.RunfilePath} for _, requirement := range task.Requires { if requirement == nil { continue @@ -43,7 +38,7 @@ func ParseTask(ctx Context, rf *Runfile, task *Task) (*ParsedTask, error) { stderr: fn.Must(os.OpenFile(os.DevNull, os.O_WRONLY, 0o755)), }) if err := cmd.Run(); err != nil { - return nil, errors.ErrTaskFailedRequirements{Context: errctx.WithErr(err), Requirement: *requirement.Sh} + return nil, errors.TaskRequirementFailed.WithErr(err).WithMetadata("requirement", *requirement.Sh).WithMetadata(attrs) } continue } @@ -54,15 +49,15 @@ func ParseTask(ctx Context, rf *Runfile, task *Task) (*ParsedTask, error) { templateExpr := fmt.Sprintf(`{{ %s }}`, *requirement.GoTmpl) t, err := t.Parse(templateExpr) if err != nil { - return nil, err + return nil, errors.TaskRequirementIncorrect.WithErr(err).WithMetadata("requirement", *requirement.GoTmpl).WithMetadata(attrs) } b := new(bytes.Buffer) if err := t.ExecuteTemplate(b, "requirement", map[string]string{}); err != nil { - return nil, err + return nil, errors.TaskRequirementIncorrect.WithErr(err).WithMetadata("requirement", *requirement.GoTmpl).WithMetadata(attrs) } if b.String() != "true" { - return nil, errors.ErrTaskFailedRequirements{Context: errctx.WithErr(fmt.Errorf("`%s` evaluated to `%s` (wanted: `true`)", templateExpr, b.String())), Requirement: *requirement.GoTmpl} + return nil, errors.TaskRequirementFailed.WithErr(fmt.Errorf("template must have evaluated to true")).WithMetadata("requirement", *requirement.GoTmpl).WithMetadata(attrs) } continue @@ -77,53 +72,39 @@ func ParseTask(ctx Context, rf *Runfile, task *Task) (*ParsedTask, error) { task.Dir = fn.New(fn.Must(os.Getwd())) } - fi, err := os.Stat(*task.Dir) - if err != nil { - return nil, errors.InvalidWorkingDirectory{Context: errctx.WithErr(err)} + fi, err2 := os.Stat(*task.Dir) + if err2 != nil { + return nil, errors.TaskWorkingDirectoryInvalid.WithErr(err2).WithMetadata("working-dir", *task.Dir).WithMetadata(attrs) } if !fi.IsDir() { - return nil, errors.InvalidWorkingDirectory{Context: errctx.WithErr(fmt.Errorf("path (%s), is not a directory", *task.Dir))} + return nil, errors.TaskWorkingDirectoryInvalid.WithErr(fmt.Errorf("path is not a directory")).WithMetadata("working-dir", *task.Dir).WithMetadata(attrs) } dotenvPaths, err := resolveDotEnvFiles(filepath.Dir(rf.attrs.RunfilePath), task.DotEnv...) if err != nil { - return nil, errors.InvalidDotEnv{Context: errctx.WithErr(err).WithMessage("failed to resolve dotenv paths")} + return nil, err.WithMetadata(attrs) } dotenvVars, err := parseDotEnvFiles(dotenvPaths...) if err != nil { - return nil, errors.InvalidDotEnv{Context: errctx.WithErr(err).WithMessage("failed to parse dotenv files")} + return nil, err.WithMetadata(attrs) } - // env := make([]string, 0, len(os.Environ())+len(dotenvVars)) - // env := make([]string, 0, len(dotenvVars)+len(task.Env)) - // env = append(env, task.Environ...) - // if !task.ignoreSystemEnv { - // env = append(env, os.Environ()...) - // } - // for k, v := range dotenvVars { - // env = append(env, fmt.Sprintf("%s=%v", k, v)) - // } - // INFO: keys from task.Env will override those coming from dotenv files, when duplicated envVars, err := parseEnvVars(ctx, task.Env, EvaluationArgs{ Shell: task.Shell, Env: dotenvVars, }) if err != nil { - return nil, errors.InvalidEnvVar{Context: errctx.WithErr(err).WithMessage("failed to parse/evaluate env vars")} + return nil, err.WithMetadata(attrs) } - // for k, v := range envVars { - // env = append(env, fmt.Sprintf("%s=%v", k, v)) - // } - commands := make([]CommandJson, 0, len(task.Commands)) for i := range task.Commands { c2, err := parseCommand(rf, task.Commands[i]) if err != nil { - return nil, errors.IncorrectCommand{Context: errctx.WithErr(err).WithMessage(fmt.Sprintf("failed to parse command: %+v", task.Commands[i]))} + return nil, err.WithMetadata(attrs) } commands = append(commands, *c2) } @@ -137,7 +118,7 @@ func ParseTask(ctx Context, rf *Runfile, task *Task) (*ParsedTask, error) { } // returns absolute paths to dotenv files -func resolveDotEnvFiles(pwd string, dotEnvFiles ...string) ([]string, error) { +func resolveDotEnvFiles(pwd string, dotEnvFiles ...string) ([]string, errors.Message) { paths := make([]string, 0, len(dotEnvFiles)) for _, v := range dotEnvFiles { @@ -147,11 +128,11 @@ func resolveDotEnvFiles(pwd string, dotEnvFiles ...string) ([]string, error) { } fi, err := os.Stat(dotenvPath) if err != nil { - return nil, err + return nil, errors.DotEnvNotFound.WithErr(err).WithMetadata("dotenv", dotenvPath) } if fi.IsDir() { - return nil, fmt.Errorf("dotenv file must be a file, but %s is a directory", v) + return nil, errors.DotEnvInvalid.WithErr(fmt.Errorf("dotenv path must be a file, but it is a directory")).WithMetadata("dotenv", dotenvPath) } paths = append(paths, dotenvPath) @@ -160,39 +141,37 @@ func resolveDotEnvFiles(pwd string, dotEnvFiles ...string) ([]string, error) { return paths, nil } -func parseCommand(rf *Runfile, command any) (*CommandJson, error) { +func parseCommand(rf *Runfile, command any) (*CommandJson, errors.Message) { switch c := command.(type) { case string: { - return &CommandJson{ - Command: c, - }, nil + return &CommandJson{Command: c}, nil } case map[string]any: { var cj CommandJson b, err := json.Marshal(c) if err != nil { - return nil, err + return nil, errors.CommandInvalid.WithErr(err).WithMetadata("command", command) } if err := json.Unmarshal(b, &cj); err != nil { - return nil, err + return nil, errors.CommandInvalid.WithErr(err).WithMetadata("command", command) } if cj.Run == "" { - return nil, fmt.Errorf("key: 'run', must be specified when setting command in json format") + return nil, errors.CommandInvalid.WithErr(fmt.Errorf("key: 'run', must be specified when setting command in json format")).WithMetadata("command", command) } if _, ok := rf.Tasks[cj.Run]; !ok { - return nil, fmt.Errorf("[run target]: %s, not found in Runfile (%s)", cj.Run, rf.attrs.RunfilePath) + return nil, errors.CommandInvalid.WithErr(fmt.Errorf("run target, not found")).WithMetadata("command", command, "run-target", cj.Run) } return &cj, nil } default: { - return nil, fmt.Errorf("invalid command") + return nil, errors.CommandInvalid.WithMetadata("command", command) } } } diff --git a/pkg/runfile/task-parser_test.go b/pkg/runfile/task-parser_test.go index 9872047..ddefa75 100644 --- a/pkg/runfile/task-parser_test.go +++ b/pkg/runfile/task-parser_test.go @@ -629,16 +629,6 @@ echo "hi" }, wantErr: true, }, - { - name: "[unhappy/runfile] target task does not exist", - args: args{ - rf: &Runfile{ - Tasks: map[string]Task{}, - }, - taskName: "test", - }, - wantErr: true, - }, } tests = append(tests, testRequires...) @@ -646,15 +636,7 @@ echo "hi" for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var task *Task - v, ok := tt.args.rf.Tasks[tt.args.taskName] - if !ok { - task = nil - } else { - task = &v - } - - got, err := ParseTask(NewContext(context.TODO(), slog.Default()), tt.args.rf, task) + got, err := ParseTask(NewContext(context.TODO(), slog.Default()), tt.args.rf, tt.args.rf.Tasks[tt.args.taskName]) if (err != nil) != tt.wantErr { t.Errorf("ParseTask(), got = %v, error = %v, wantErr %v", got, err, tt.wantErr) return