Skip to content

Commit

Permalink
feat: adds requires conditions for task
Browse files Browse the repository at this point in the history
json environments are full featured now with `default`, `required`, and
templated values
  • Loading branch information
nxtcoder17 committed Sep 28, 2024
1 parent d4fad9a commit 5bc0617
Show file tree
Hide file tree
Showing 10 changed files with 537 additions and 86 deletions.
27 changes: 27 additions & 0 deletions docs/requirements-for-a-run-target.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
Github Issue: https://github.com/nxtcoder17/Runfile/issues/8
---

### Expectations from this implementation ?

I would like to be able to do stuffs like:
- whether these environment variables have been defined
- whether this filepath exists or not
- whether `this command` or script runs sucessfully

And, when answers to these questsions are `true`, then only run the target, otherwise throw the errors

### Problems with Taskfile.dev implementation

They have 2 ways to tackle this, with

- `requires`: but, it works with vars only, ~no environment variables~

- `preconditions`: test conditions must be a valid linux `test` command, which assumes everyone knows how to read bash's `test` or `if` statements


### My Approach

1. Support for `test` commands, must be there, for advanced users

2. But, for simpler use cases, there should be alternate ways to do it, something that whole team just understands.
2 changes: 2 additions & 0 deletions examples/Runfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ tasks:
k2: 'f"\( asfsadfssdfas asfd $Asdfasdfa'
k3:
sh: echo -n "hello"
k4:
required: true
dotenv:
- ../.secrets/env
cmd:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/nxtcoder17/runfile
go 1.22.7

require (
github.com/go-task/slim-sprig/v3 v3.0.0
github.com/joho/godotenv v1.5.1
github.com/nxtcoder17/fwatcher v1.0.1
github.com/urfave/cli/v3 v3.0.0-alpha9
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
Expand Down
126 changes: 126 additions & 0 deletions pkg/runfile/errors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package errors

import (
"encoding/json"
"fmt"
)

type Context struct {
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 {
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()
}
6 changes: 5 additions & 1 deletion pkg/runfile/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"os/exec"

"github.com/nxtcoder17/runfile/pkg/runfile/errors"
"golang.org/x/sync/errgroup"
)

Expand Down Expand Up @@ -85,7 +86,10 @@ type RunArgs struct {
func (rf *Runfile) Run(ctx context.Context, args RunArgs) error {
for _, v := range args.Tasks {
if _, ok := rf.Tasks[v]; !ok {
return ErrTaskNotFound{TaskName: v, RunfilePath: rf.attrs.RunfilePath}
return errors.TaskNotFound{Context: errors.Context{
Task: v,
Runfile: rf.attrs.RunfilePath,
}}
}
}

Expand Down
88 changes: 55 additions & 33 deletions pkg/runfile/task-parser.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package runfile

import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"text/template"

sprig "github.com/go-task/slim-sprig/v3"
fn "github.com/nxtcoder17/runfile/pkg/functions"
"github.com/nxtcoder17/runfile/pkg/runfile/errors"
)

type ParsedTask struct {
Expand All @@ -17,35 +21,53 @@ type ParsedTask struct {
Commands []CommandJson `json:"commands"`
}

type ErrTask struct {
ErrTaskNotFound
Message string
Err error
}

func (e ErrTask) Error() string {
return fmt.Sprintf("[task] %s (Runfile: %s), says %s, got %v", e.TaskName, e.RunfilePath, e.Message, e.Err)
}

func NewErrTask(runfilePath, taskName, message string, err error) ErrTask {
return ErrTask{
ErrTaskNotFound: ErrTaskNotFound{
RunfilePath: runfilePath,
TaskName: taskName,
},
Message: message,
Err: err,
}
}

func ParseTask(ctx context.Context, rf *Runfile, taskName string) (*ParsedTask, error) {
newErr := func(message string, err error) ErrTask {
return NewErrTask(rf.attrs.RunfilePath, taskName, message, err)
}
errctx := errors.Context{Task: taskName, Runfile: rf.attrs.RunfilePath}

task, ok := rf.Tasks[taskName]
if !ok {
return nil, ErrTaskNotFound{TaskName: taskName, RunfilePath: rf.attrs.RunfilePath}
return nil, errors.TaskNotFound{Context: errctx}
}

for _, requirement := range task.Requires {
if requirement == nil {
continue
}

if requirement.Sh != nil {
cmd := createCommand(ctx, cmdArgs{
shell: []string{"sh", "-c"},
env: os.Environ(),
workingDir: filepath.Dir(rf.attrs.RunfilePath),
cmd: *requirement.Sh,
stdout: fn.Must(os.OpenFile(os.DevNull, os.O_WRONLY, 0o755)),
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}
}
continue
}

if requirement.GoTmpl != nil {
t := template.New("requirement")
t = t.Funcs(sprig.FuncMap())
templateExpr := fmt.Sprintf(`{{ %s }}`, *requirement.GoTmpl)
t, err := t.Parse(templateExpr)
if err != nil {
return nil, err
}
b := new(bytes.Buffer)
if err := t.ExecuteTemplate(b, "requirement", map[string]string{}); err != nil {
return nil, err
}

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}
}

continue
}
}

if task.Shell == nil {
Expand All @@ -58,21 +80,21 @@ func ParseTask(ctx context.Context, rf *Runfile, taskName string) (*ParsedTask,

fi, err := os.Stat(*task.Dir)
if err != nil {
return nil, newErr("invalid directory", err)
return nil, errors.InvalidWorkingDirectory{Context: errctx.WithErr(err)}
}

if !fi.IsDir() {
return nil, newErr("invalid working directory", fmt.Errorf("path (%s), is not a directory", *task.Dir))
return nil, errors.InvalidWorkingDirectory{Context: errctx.WithErr(fmt.Errorf("path (%s), is not a directory", *task.Dir))}
}

dotenvPaths, err := resolveDotEnvFiles(filepath.Dir(rf.attrs.RunfilePath), task.DotEnv...)
if err != nil {
return nil, newErr("while resolving dotenv paths", err)
return nil, errors.InvalidDotEnv{Context: errctx.WithErr(err).WithMessage("failed to resolve dotenv paths")}
}

dotenvVars, err := parseDotEnvFiles(dotenvPaths...)
if err != nil {
return nil, newErr("while parsing dotenv files", err)
return nil, errors.InvalidDotEnv{Context: errctx.WithErr(err).WithMessage("failed to parse dotenv files")}
}

env := make([]string, 0, len(os.Environ())+len(dotenvVars))
Expand All @@ -85,11 +107,11 @@ func ParseTask(ctx context.Context, rf *Runfile, taskName string) (*ParsedTask,

// INFO: keys from task.Env will override those coming from dotenv files, when duplicated
envVars, err := parseEnvVars(ctx, task.Env, EvaluationArgs{
Shell: task.Shell,
Environ: env,
Shell: task.Shell,
Env: dotenvVars,
})
if err != nil {
return nil, newErr("while evaluating task env vars", err)
return nil, errors.InvalidEnvVar{Context: errctx.WithErr(err).WithMessage("failed to parse/evaluate env vars")}
}

for k, v := range envVars {
Expand All @@ -100,7 +122,7 @@ func ParseTask(ctx context.Context, rf *Runfile, taskName string) (*ParsedTask,
for i := range task.Commands {
c2, err := parseCommand(rf, task.Commands[i])
if err != nil {
return nil, newErr("while parsing command", err)
return nil, errors.IncorrectCommand{Context: errctx.WithErr(err).WithMessage(fmt.Sprintf("failed to parse command: %+v", task.Commands[i]))}
}
commands = append(commands, *c2)
}
Expand Down
Loading

0 comments on commit 5bc0617

Please sign in to comment.